diff --git a/.github/actions/build-electron/action.yml b/.github/actions/build-electron/action.yml index fe895b409..b55ffea4b 100644 --- a/.github/actions/build-electron/action.yml +++ b/.github/actions/build-electron/action.yml @@ -15,13 +15,22 @@ runs: if: ${{ inputs.os == 'macos' }} shell: bash run: brew install python-setuptools - - name: Install rpm on Ubuntu for RPM package building + - name: Install dependencies for RPM and Flatpak package building if: ${{ inputs.os == 'linux' }} shell: bash - run: sudo apt install rpm + run: | + sudo apt-get update && sudo apt-get install rpm flatpak-builder elfutils + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + FLATPAK_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi) + FLATPAK_VERSION='24.08' + flatpak install --user --no-deps --arch $FLATPAK_ARCH --assumeyes runtime/org.freedesktop.Platform/$FLATPAK_ARCH/$FLATPAK_VERSION runtime/org.freedesktop.Sdk/$FLATPAK_ARCH/$FLATPAK_VERSION org.electronjs.Electron2.BaseApp/$FLATPAK_ARCH/$FLATPAK_VERSION - name: Install dependencies shell: bash run: npm ci + - name: Temporary Flatpak arm64 workaround till https://github.com/electron/forge/pull/3839 is merged + if: ${{ inputs.os == 'linux' && inputs.arch == 'arm64' }} + shell: bash + run: sed -e "s/case 'armv7l'/case 'arm64'/g" -e "s/return 'arm'/return 'aarch64'/g" -i node_modules/@electron-forge/maker-flatpak/dist/MakerFlatpak.js - name: Update build info shell: bash run: npm run update-build-info diff --git a/.github/actions/build-server/action.yml b/.github/actions/build-server/action.yml index 162116e87..694005c1b 100644 --- a/.github/actions/build-server/action.yml +++ b/.github/actions/build-server/action.yml @@ -25,4 +25,4 @@ runs: run: | mkdir -p upload file=$(find dist -name '*.tar.xz' -print -quit) - cp "$file" "upload/TriliumNextNotes-linux-${{ inputs.arch }}-${{ github.ref_name }}.tar.xz" + cp "$file" "upload/TriliumNextNotes-Server-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b9ec71b1..b19145ca1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: extension: [dmg, zip] - name: linux image: ubuntu-latest - extension: [deb, rpm, zip] + extension: [deb, rpm, zip, flatpak] - name: windows image: windows-latest extension: exe @@ -53,6 +53,7 @@ jobs: with: name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}} path: upload/*.${{ matrix.os.extension }} + build_linux_server: name: Build Linux Server strategy: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 22a6b26b6..2b1fbe57e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -23,7 +23,7 @@ jobs: extension: [dmg, zip] - name: linux image: ubuntu-latest - extension: [deb, rpm, zip] + extension: [deb, rpm, zip, flatpak] - name: windows image: windows-latest extension: exe @@ -49,11 +49,14 @@ jobs: - name: Publish release uses: softprops/action-gh-release@v2 with: - draft: true + make_latest: false + prerelease: true + draft: false fail_on_unmatched_files: true files: upload/*.* tag_name: nightly name: Nightly Build + nightly-server: name: Deploy server nightly strategy: @@ -77,7 +80,9 @@ jobs: - name: Publish release uses: softprops/action-gh-release@v2 with: - draft: true + make_latest: false + prerelease: true + draft: false fail_on_unmatched_files: true files: upload/*.* tag_name: nightly diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5cfcae17..b7625a706 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: extension: [dmg, zip] - name: linux image: ubuntu-latest - extension: [deb, rpm, zip] + extension: [deb, rpm, zip, flatpak] - name: windows image: windows-latest extension: exe @@ -46,6 +46,7 @@ jobs: draft: true fail_on_unmatched_files: true files: upload/*.* + build_linux_server-x64: name: Build Linux Server strategy: diff --git a/.gitignore b/.gitignore index 09804755d..bbfb24483 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ src/public/app-dist/ npm-debug.log yarn-error.log po-*/ +.flatpak-builder/ *.db !integration-tests/db/document.db diff --git a/README.md b/README.md index 1800f667e..e2a81aa4c 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,10 @@ Head on over to our [Docs repo](https://github.com/TriliumNext/Docs) ## 🤝 Support -You can support the original Trilium developer using GitHub Sponsors, [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2). -Support for the TriliumNext organization will be possible in the near future. +Support for the TriliumNext organization will be possible in the near future. For now, you can: +- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list) +- Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2). + ## 🔑 License diff --git a/db/demo.zip b/db/demo.zip index b972fedd2..39c0875fe 100644 Binary files a/db/demo.zip and b/db/demo.zip differ diff --git a/forge.config.cjs b/forge.config.cjs index b3765a35b..91000d63c 100644 --- a/forge.config.cjs +++ b/forge.config.cjs @@ -7,6 +7,7 @@ const extraResourcesForPlatform = getExtraResourcesForPlatform(); const baseLinuxMakerConfigOptions = { icon: "./images/app-icons/png/128x128.png", desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs"), + categories: ["Office", "Utility"] }; module.exports = { @@ -59,6 +60,29 @@ module.exports = { } } }, + { + name: "@electron-forge/maker-flatpak", + config: { + options: { + ...baseLinuxMakerConfigOptions, + id: "com.triliumnext.notes", + runtimeVersion: "24.08", + base: "org.electronjs.Electron2.BaseApp", + baseVersion: "24.08", + baseFlatpakref: "https://flathub.org/repo/flathub.flatpakrepo", + modules: [ + { + name: "zypak", + sources: { + type: "git", + url: "https://github.com/refi64/zypak", + tag: "v2024.01.17" + } + } + ] + }, + } + }, { name: "@electron-forge/maker-rpm", config: { diff --git a/images/app-icons/win/setup.ico b/images/app-icons/win/setup.ico index 34dd447c5..671c19e7a 100644 Binary files a/images/app-icons/win/setup.ico and b/images/app-icons/win/setup.ico differ diff --git a/images/icon-installer.svg b/images/icon-installer.svg index 9edc6b331..3886e7904 100644 --- a/images/icon-installer.svg +++ b/images/icon-installer.svg @@ -1,124 +1,125 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 80f8f05b1..c945393fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,6 +108,7 @@ "@electron-forge/cli": "7.6.1", "@electron-forge/maker-deb": "7.6.1", "@electron-forge/maker-dmg": "7.6.1", + "@electron-forge/maker-flatpak": "7.6.1", "@electron-forge/maker-rpm": "7.6.1", "@electron-forge/maker-squirrel": "7.6.1", "@electron-forge/maker-zip": "7.6.1", @@ -152,7 +153,7 @@ "@types/yargs": "17.0.33", "@vitest/coverage-v8": "3.0.5", "cross-env": "7.0.3", - "electron": "34.0.2", + "electron": "34.1.0", "esm": "3.2.25", "jasmine": "5.5.0", "jsdoc": "4.0.4", @@ -781,6 +782,52 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/@electron-forge/maker-flatpak": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-flatpak/-/maker-flatpak-7.6.1.tgz", + "integrity": "sha512-a9EekF8cNzjizwMs8HObxRii2tkLrTcTNMvWNhQqcJohEkJV81zNOLu9l/OdIMlKQ9cF5SuRvA4/m2bQGfT80w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.6.1", + "@electron-forge/shared-types": "7.6.1", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "@malept/electron-installer-flatpak": "^0.11.4" + } + }, + "node_modules/@electron-forge/maker-flatpak/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron-forge/maker-flatpak/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/@electron-forge/maker-rpm": { "version": "7.6.1", "resolved": "https://registry.npmjs.org/@electron-forge/maker-rpm/-/maker-rpm-7.6.1.tgz", @@ -2752,6 +2799,203 @@ "node": ">= 12.13.0" } }, + "node_modules/@malept/electron-installer-flatpak": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/@malept/electron-installer-flatpak/-/electron-installer-flatpak-0.11.4.tgz", + "integrity": "sha512-ZdwhT4WeeJWdnsmALUtQ7bn4pzYVh0Vg+4NnF1S3n3OACc9IWg+B+LxI5gT3XSXIrxogouqkjM6gD8S592awyA==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "@malept/flatpak-bundler": "^0.4.0", + "debug": "^4.1.1", + "electron-installer-common": "^0.10.0", + "lodash": "^4.17.15", + "semver": "^7.1.1", + "yargs": "^16.0.0" + }, + "bin": { + "electron-installer-flatpak": "bin/electron-installer-flatpak.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/electron-installer-flatpak/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/@mermaid-js/layout-elk": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/@mermaid-js/layout-elk/-/layout-elk-0.1.7.tgz", @@ -7666,9 +7910,9 @@ } }, "node_modules/electron": { - "version": "34.0.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.0.2.tgz", - "integrity": "sha512-u3F+DSUlg9NaGS+9qnYmSRN8VjAnc3LJDDk1ye1uISJnh4gjG76y3681qLowsPMx4obvCP2eBINnmbLo0yT5WA==", + "version": "34.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.1.0.tgz", + "integrity": "sha512-ZUid8XrGPA0dfes97PPADc8ecWOUX/qYRNp1glze9coZLEYc+PsMvgjVDCHSvjfHfiI+V3unwngSVpBouX71YQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -8387,9 +8631,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.16.tgz", - "integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==", + "version": "20.17.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", + "integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" diff --git a/package.json b/package.json index 8bdc25874..47b2337a1 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "@electron-forge/cli": "7.6.1", "@electron-forge/maker-deb": "7.6.1", "@electron-forge/maker-dmg": "7.6.1", + "@electron-forge/maker-flatpak": "7.6.1", "@electron-forge/maker-rpm": "7.6.1", "@electron-forge/maker-squirrel": "7.6.1", "@electron-forge/maker-zip": "7.6.1", @@ -194,7 +195,7 @@ "@types/yargs": "17.0.33", "@vitest/coverage-v8": "3.0.5", "cross-env": "7.0.3", - "electron": "34.0.2", + "electron": "34.1.0", "esm": "3.2.25", "jasmine": "5.5.0", "jsdoc": "4.0.4", diff --git a/spec/support/becca_mocking.ts b/spec/support/becca_mocking.ts index 0c65f16b6..db2382cd5 100644 --- a/spec/support/becca_mocking.ts +++ b/spec/support/becca_mocking.ts @@ -4,14 +4,14 @@ import BAttribute from "../../src/becca/entities/battribute.js"; import becca from "../../src/becca/becca.js"; import randtoken from "rand-token"; import type SearchResult from "../../src/services/search/search_result.js"; -import type { NoteType } from "../../src/becca/entities/rows.js"; +import type { NoteRow, NoteType } from "../../src/becca/entities/rows.js"; randtoken.generator({ source: "crypto" }); -function findNoteByTitle(searchResults: Array, title: string): BNote | undefined { +export function findNoteByTitle(searchResults: Array, title: string): BNote | undefined { return searchResults.map((sr) => becca.notes[sr.noteId]).find((note) => note.title === title); } -class NoteBuilder { +export class NoteBuilder { note: BNote; constructor(note: BNote) { this.note = note; @@ -55,11 +55,11 @@ class NoteBuilder { } } -function id() { +export function id() { return randtoken.generate(10); } -function note(title: string, extraParams = {}) { +export function note(title: string, extraParams: Partial = {}) { const row = Object.assign( { noteId: id(), @@ -74,9 +74,3 @@ function note(title: string, extraParams = {}) { return new NoteBuilder(note); } - -export default { - NoteBuilder, - findNoteByTitle, - note -}; diff --git a/src/public/app/services/attribute_renderer.ts b/src/public/app/services/attribute_renderer.ts index 6c7b9412d..f09ecf8f9 100644 --- a/src/public/app/services/attribute_renderer.ts +++ b/src/public/app/services/attribute_renderer.ts @@ -79,7 +79,7 @@ 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", "geolocation"]; async function renderNormalAttributes(note: FNote) { const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); diff --git a/src/public/app/services/content_renderer.ts b/src/public/app/services/content_renderer.ts index 546d836d2..18aafdb4f 100644 --- a/src/public/app/services/content_renderer.ts +++ b/src/public/app/services/content_renderer.ts @@ -238,12 +238,16 @@ async function renderMermaid(note: FNote, $renderedContent: JQuery) * @param {FNote} note * @returns {Promise} */ -async function renderChildrenList($renderedContent: JQuery, note: FNote) { +async function renderChildrenList($renderedContent: JQuery, note: FNote) { + let childNoteIds = note.getChildNoteIds(); + + if (!childNoteIds.length) { + return; + } + $renderedContent.css("padding", "10px"); $renderedContent.addClass("text-with-ellipsis"); - let childNoteIds = note.getChildNoteIds(); - if (childNoteIds.length > 10) { childNoteIds = childNoteIds.slice(0, 10); } diff --git a/src/public/app/services/link.ts b/src/public/app/services/link.ts index 581f30ff6..2edccdd55 100644 --- a/src/public/app/services/link.ts +++ b/src/public/app/services/link.ts @@ -376,6 +376,10 @@ function linkContextMenu(e: PointerEvent) { const $link = $(e.target as any).closest("a"); const url = $link.attr("href") || $link.attr("data-href"); + if ($link.attr("data-no-context-menu")) { + return; + } + const { notePath, viewScope } = parseNavigationStateFromUrl(url); if (!notePath) { diff --git a/src/public/app/services/note_tooltip.ts b/src/public/app/services/note_tooltip.ts index 179ded65a..28937f3e2 100644 --- a/src/public/app/services/note_tooltip.ts +++ b/src/public/app/services/note_tooltip.ts @@ -140,10 +140,6 @@ async function renderTooltip(note: FNote | null) { } const noteTitleWithPathAsSuffix = await treeService.getNoteTitleWithPathAsSuffix(bestNotePath); - let content = ""; - if (noteTitleWithPathAsSuffix) { - content = `${noteTitleWithPathAsSuffix.prop("outerHTML")}`; - } const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note); @@ -151,8 +147,21 @@ async function renderTooltip(note: FNote | null) { tooltip: true, trim: true }); + const isContentEmpty = ($renderedContent[0].innerHTML.length === 0); - content = `${content}${$renderedAttributes[0].outerHTML}${$renderedContent[0].outerHTML}`; + let content = ""; + if (noteTitleWithPathAsSuffix) { + const classes = [ "note-tooltip-title" ]; + if (isContentEmpty) { + classes.push("note-no-content"); + } + content = `${noteTitleWithPathAsSuffix.prop("outerHTML")}`; + } + + content = `${content}${$renderedAttributes[0].outerHTML}`; + if (!isContentEmpty) { + content += $renderedContent[0].outerHTML; + } return content; } diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.ts b/src/public/app/widgets/attribute_widgets/attribute_detail.ts index c5c56102e..866cff805 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.ts +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.ts @@ -13,7 +13,7 @@ import appContext from "../../components/app_context.js"; import type { Attribute } from "../../services/attribute_parser.js"; const TPL = ` - + @@ -74,11 +96,16 @@ export default class NoteTypeWidget extends NoteContextAwareWidget { for (const noteType of NOTE_TYPES.filter((nt) => nt.selectable)) { let $typeLink; + const $title = $("").text(noteType.title); + if (noteType.isBeta) { + $title.append($(``).text(t("note_types.beta-feature"))); + } + if (noteType.type !== "code") { $typeLink = $('') .attr("data-note-type", noteType.type) .append('✓ ') - .append($("").text(noteType.title)) + .append($title) .on("click", (e) => { const type = $typeLink.attr("data-note-type"); const noteType = NOTE_TYPES.find((nt) => nt.type === type); diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index ddd037673..daaeca669 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -42,6 +42,7 @@ const TAB_ROW_TPL = ` width: 100%; background: var(--main-background-color); overflow: hidden; + user-select: none; } .tab-row-widget.full-width { diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.js index 15932d63b..108c5da2e 100644 --- a/src/public/app/widgets/type_widgets/attachment_detail.js +++ b/src/public/app/widgets/type_widgets/attachment_detail.js @@ -48,11 +48,12 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { this.$wrapper.empty(); this.children = []; - const $helpButton = $( - '' - ); + const $helpButton = $(` + + `); utils.initHelpButtons($helpButton); this.$linksWrapper.empty().append( diff --git a/src/public/app/widgets/type_widgets/attachment_list.js b/src/public/app/widgets/type_widgets/attachment_list.js index db5d44771..f6eadb97e 100644 --- a/src/public/app/widgets/type_widgets/attachment_list.js +++ b/src/public/app/widgets/type_widgets/attachment_list.js @@ -40,11 +40,12 @@ export default class AttachmentListTypeWidget extends TypeWidget { } async doRefresh(note) { - const $helpButton = $( - '' - ); + const $helpButton = $(` + + + `); utils.initHelpButtons($helpButton); const noteLink = await linkService.createLink(this.noteId); // do separately to avoid race condition between empty() and .append() @@ -52,7 +53,7 @@ export default class AttachmentListTypeWidget extends TypeWidget { this.$linksWrapper.empty().append( $("").append(t("attachment_list.owning_note"), noteLink), - $("").append( + $(``).append( $('') .text(t("attachment_list.upload_attachments")) .on("click", () => this.triggerCommand("showUploadAttachmentsDialog", { noteId: this.noteId })), diff --git a/src/public/app/widgets/type_widgets/geo_map.ts b/src/public/app/widgets/type_widgets/geo_map.ts index fdca64cbc..11de0b2b3 100644 --- a/src/public/app/widgets/type_widgets/geo_map.ts +++ b/src/public/app/widgets/type_widgets/geo_map.ts @@ -12,6 +12,7 @@ import asset_path from "../../../../services/asset_path.js"; import openContextMenu from "./geo_map_context_menu.js"; import link from "../../services/link.js"; import note_tooltip from "../../services/note_tooltip.js"; +import appContext from "../../components/app_context.js"; const TPL = `\ @@ -229,7 +230,15 @@ export default class GeoMapTypeWidget extends TypeWidget { .on("moveend", e => { this.moveMarker(note.noteId, (e.target as Marker).getLatLng()); }); - + marker.on("mousedown", ({ originalEvent }) => { + // Middle click to open in new tab + if (originalEvent.button === 1) { + const hoistedNoteId = this.hoistedNoteId; + //@ts-ignore, fix once tab manager is ported. + appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId); + return true; + } + }); marker.on("contextmenu", (e) => { openContextMenu(note.noteId, e.originalEvent); }); diff --git a/src/public/stylesheets/calendar.css b/src/public/stylesheets/calendar.css index 49de32f52..3d6d34114 100644 --- a/src/public/stylesheets/calendar.css +++ b/src/public/stylesheets/calendar.css @@ -41,25 +41,41 @@ flex-grow: 1; } +.calendar-dropdown-widget .calendar-header input[type="number"] { + appearance: textfield !important; +} + +.calendar-dropdown-widget .calendar-header input[type="number"]::-webkit-outer-spin-button, +.calendar-dropdown-widget .calendar-header input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + .calendar-dropdown-widget .calendar-header select { cursor: pointer; } .calendar-dropdown-widget .calendar-header input, -.calendar-dropdown-widget .calendar-header select { +.calendar-dropdown-widget .calendar-header .dropdown-toggle { appearance: none; text-align: center; border: 0; - border-bottom: 1px solid var(--button-border-color); - background-color: var(--menu-background-color) !important; + border-left: unset; + background-color: var(--menu-background-color); + font-weight: bold; outline: 0; } +.calendar-dropdown-widget .calendar-header .dropdown-toggle::after { + border: unset; /* Disable the dropdown arrow */ +} + .calendar-dropdown-widget .calendar-week { display: flex; flex-wrap: wrap; } + .calendar-dropdown-widget .calendar-week span { flex-direction: column; flex: 0 0 14.28%; @@ -69,6 +85,7 @@ padding-top: 5px; padding-bottom: 5px; text-align: center; + text-transform: uppercase; } .calendar-dropdown-widget .calendar-body { @@ -79,7 +96,6 @@ .calendar-dropdown-widget .calendar-date { align-items: center; color: var(--main-text-color); - background-color: var(--main-background-color); display: flex; flex-direction: column; flex: 0 0 14.28%; @@ -91,6 +107,7 @@ .calendar-dropdown-widget .calendar-date:hover { color: var(--hover-item-text-color); background-color: var(--hover-item-background-color); + text-decoration: underline; } .calendar-dropdown-widget .calendar-date-active { diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 46ec9da4e..c74641656 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -1536,15 +1536,16 @@ textarea { display: none !important; } +.attachment-actions-toolbar { + display: flex; + align-items: center; +} + .attachment-help-button { - font-size: xx-large; - border: 0; - background: none; - cursor: pointer; - color: var(--main-text-color); - margin-left: 20px; - position: relative; - top: 8px; + display: inline-block; + margin-left: 10px; + vertical-align: middle; + font-size: 1em; } .jump-to-note-dialog .modal-header { diff --git a/src/public/stylesheets/theme-next-dark.css b/src/public/stylesheets/theme-next-dark.css index 6be97ced9..b9e92843e 100644 --- a/src/public/stylesheets/theme-next-dark.css +++ b/src/public/stylesheets/theme-next-dark.css @@ -19,6 +19,9 @@ --accented-background-color: #555; + --tool-dialog-background-color: #262626; + --tool-dialog-shadow-color: black; + --button-text-color: currentColor; --cmd-button-background-color: #ffffff28; diff --git a/src/public/stylesheets/theme-next-light.css b/src/public/stylesheets/theme-next-light.css index 1227ed31e..7cd90272c 100644 --- a/src/public/stylesheets/theme-next-light.css +++ b/src/public/stylesheets/theme-next-light.css @@ -19,6 +19,9 @@ --accented-background-color: #f5f5f5; + --tool-dialog-background-color: white; + --tool-dialog-shadow-color: #00000070; + --button-text-color: currentColor; --cmd-button-background-color: #0000000f; @@ -75,7 +78,7 @@ --menu-text-color: #272727; --menu-background-color: #ffffffd9; --menu-item-icon-color: #727272; - --menu-item-disabled-opacity: 0.5; + --menu-item-disabled-opacity: 0.6; --menu-item-keyboard-shortcut-color: #666666a8; --menu-item-arrow-color: #00000080; --menu-item-delimiter-color: #00000030; diff --git a/src/public/stylesheets/theme-next/base.css b/src/public/stylesheets/theme-next/base.css index e6d02dff3..3d5a8eee0 100644 --- a/src/public/stylesheets/theme-next/base.css +++ b/src/public/stylesheets/theme-next/base.css @@ -67,6 +67,14 @@ --tab-note-icons: true; } +/* Tool dialogs - small dialogs without a backdrop */ +div.tn-tool-dialog { + border-radius: 10px; + background: var(--tool-dialog-background-color) !important; + user-select: none; + box-shadow: 10px 10px 93px -25px var(--tool-dialog-shadow-color); +} + /* * Note search suggestions */ diff --git a/src/public/stylesheets/theme-next/forms.css b/src/public/stylesheets/theme-next/forms.css index 02b2ba842..293b45680 100644 --- a/src/public/stylesheets/theme-next/forms.css +++ b/src/public/stylesheets/theme-next/forms.css @@ -141,10 +141,12 @@ input[type="text"], input[type="number"], input[type="password"], textarea.form-control, -textarea { +textarea, +.tn-input-field { outline: 3px solid transparent; outline-offset: 6px; border: unset; + border-radius: var(--bs-border-radius); background: var(--input-background-color); color: var(--input-text-color); } @@ -154,7 +156,8 @@ input[type="text"]:hover, input[type="number"]:hover, input[type="password"]:hover, textarea.form-control:hover, -textarea:hover { +textarea:hover, +.tn-input-field:hover { background: var(--input-hover-background); color: var(--input-hover-color); } @@ -164,7 +167,9 @@ input[type="text"]:focus, input[type="number"]:focus, input[type="password"]:focus, textarea.form-control:focus, -textarea:focus { +textarea:focus, +.tn-input-field:focus, +.tn-input-field:focus-within { box-shadow: unset; outline: 3px solid var(--input-focus-outline-color); outline-offset: 0; @@ -337,6 +342,53 @@ optgroup { line-height: 40px; } +/* + * File input + * + * + * + * + */ + + .tn-file-input { + position: relative; + padding: .375rem 2.25rem .375rem .75rem; +} + +.tn-file-input input[type="file"] { + background: transparent; +} + +.tn-file-input input[type="file"]::file-selector-button { + /* Hide the "Browse..." button */ + display: none; +} + +.tn-file-input:hover input[type="file"] { + color: var(--input-hover-color); +} + +.tn-file-input input[type="file"]:focus, +.tn-file-input input[type="file"]:active { + outline: none; + color: var(--input-focus-color); +} + +/* The browse icon */ +.tn-file-input::before { + display: flex; + position: absolute; + justify-content: center; + align-items: center; + content: "\eae1"; + width: 2em; + height: 100%; + right: 0; + top: 0; + font-size: 1.2em; + font-family: boxicons; +} + /* Check boxes and radio buttons */ @supports selector(label:has(*)) { diff --git a/src/public/stylesheets/theme-next/ribbon.css b/src/public/stylesheets/theme-next/ribbon.css index 592c46397..4213a665e 100644 --- a/src/public/stylesheets/theme-next/ribbon.css +++ b/src/public/stylesheets/theme-next/ribbon.css @@ -1,3 +1,29 @@ +/* + * Basic properties + */ + +/* Note type dropdown */ + +div.note-type-dropdown .check { + margin-right: 6px; +} + + /* Editability dropdown */ + +div.editability-dropdown a.dropdown-item { + padding: 4px 16px 4px 0; + align-items: start !important; +} + +.editability-dropdown .dropdown-item .check { + margin-left: 4px; +} + +.editability-dropdown .dropdown-item .description { + opacity: .75; + font-size: .85em; +} + /* * Owned attributes */ @@ -11,6 +37,11 @@ right: 30px; } +/* Note path in attribute detail dialog */ +.attr-detail .note-path { + margin-left: 8px; +} + /* * Similar notes */ diff --git a/src/public/stylesheets/theme-next/settings.css b/src/public/stylesheets/theme-next/settings.css index 4a77f6228..e372f3c40 100644 --- a/src/public/stylesheets/theme-next/settings.css +++ b/src/public/stylesheets/theme-next/settings.css @@ -79,4 +79,10 @@ .detail-font-size-input-group, .monospace-font-size-input-group { width: fit-content; +} + +/* Advanced */ + +.options-section ul.existing-anonymized-databases { + margin: 1em; } \ No newline at end of file diff --git a/src/public/stylesheets/theme-next/shell.css b/src/public/stylesheets/theme-next/shell.css index e39771edb..acf7818f0 100644 --- a/src/public/stylesheets/theme-next/shell.css +++ b/src/public/stylesheets/theme-next/shell.css @@ -956,7 +956,7 @@ body.mobile .note-title { * supported when this class is used. */ -.dropdown-menu { +.dropdown-menu:not(.static) { border-radius: var(--dropdown-border-radius); padding: var(--menu-padding-size) !important; font-size: 0.9rem !important; @@ -1034,7 +1034,8 @@ body.mobile .dropdown-submenu:hover { background: transparent !important; } -html body .dropdown-item.disabled { +html body .dropdown-item.disabled, +html body .dropdown-item[disabled] { color: var(--menu-text-color) !important; opacity: var(--menu-item-disabled-opacity); } @@ -1087,6 +1088,20 @@ html body .dropdown-item.disabled { color: var(--menu-item-arrow-color) !important; } +/* Static menus (used as a list, such as on the note revisions dialog) */ +body.desktop .dropdown-menu.static { + box-shadow: unset; + border-radius: 4px; + border: unset; + background-color: var(--card-background-color) !important; + padding: var(--menu-padding-size) !important; + user-select: none; +} + +body.desktop .dropdown-menu.static .dropdown-item.active { + --active-item-text-color: var(--menu-text-color); +} + body.desktop .dropdown-menu .dropdown-toggle::after { height: 100%; } @@ -1104,34 +1119,38 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after { */ .calendar-dropdown-widget { + width: unset !important; padding: 12px; color: var(--calendar-color); + user-select: none; } .calendar-dropdown-widget .calendar-header { padding: 8px 0 20px 0; -} - -.calendar-dropdown-widget .calendar-header input[type="number"] { - appearance: textfield !important; -} - -.calendar-dropdown-widget .calendar-header input[type="number"]::-webkit-outer-spin-button, -.calendar-dropdown-widget .calendar-header input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; + gap: 10px; } .calendar-dropdown-widget .calendar-header input, .calendar-dropdown-widget .calendar-header [data-calendar-input="month"] { - /* TODO: Provide styling for background and states */ - background: transparent !important; - border: unset; + --input-background-color: transparent; + --menu-background-color: transparent; + text-align: center; font-size: 1.4em; font-weight: 300; } +.calendar-dropdown-widget .calendar-header input:not(:focus) { + outline: 3px solid transparent; +} + +.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button { + --select-arrow-svg: ""; /* Disable the dropdown arrow */ + + min-width: 120px; + padding: 0 10px; +} + .calendar-dropdown-widget .calendar-header .dropdown-toggle::after { content: unset !important; } @@ -1174,6 +1193,7 @@ body .calendar-dropdown-widget .calendar-body a:hover { border-radius: 6px; background: var(--calendar-day-hover-background); color: var(--calendar-day-hover-color) !important; + text-decoration: unset; } /* @@ -1186,13 +1206,25 @@ body .calendar-dropdown-widget .calendar-body a:hover { .note-tooltip-content { padding: 8px; + min-height: 56px; } -.note-tooltip-content .note-title-with-path { +.note-tooltip-title .note-title-with-path { display: flex; flex-direction: column-reverse; - border-bottom: 2px solid currentColor; +} + +.note-tooltip-title a { + color: inherit !important; +} + +.note-tooltip-title.note-no-content { + margin: 0; +} + +.note-tooltip-title:not(.note-no-content) .note-title-with-path { padding-bottom: 6px; + border-bottom: 2px solid currentColor; } .note-tooltip-content .note-path { diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 4fcd77b1e..36fdccb4d 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1415,7 +1415,8 @@ "doc": "Doc", "widget": "Widget", "confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?", - "geo-map": "Geo Map (beta)" + "geo-map": "Geo Map", + "beta-feature": "Beta" }, "protect_note": { "toggle-on": "Protect the note", diff --git a/src/public/translations/ro/translation.json b/src/public/translations/ro/translation.json index 280a00302..9f4c1ade9 100644 --- a/src/public/translations/ro/translation.json +++ b/src/public/translations/ro/translation.json @@ -1381,7 +1381,8 @@ "launcher": "Scurtătură", "widget": "Widget", "confirm-change": "Nu se recomandă schimbarea tipului notiței atunci când ea are un conținut. Procedați oricum?", - "geo-map": "Hartă geografică (beta)" + "geo-map": "Hartă geografică", + "beta-feature": "Beta" }, "protect_note": { "toggle-off": "Deprotejează notița", diff --git a/src/services/search/services/search.spec.ts b/src/services/search/services/search.spec.ts index ce9acb3b0..e468ffd07 100644 --- a/src/services/search/services/search.spec.ts +++ b/src/services/search/services/search.spec.ts @@ -5,7 +5,7 @@ import BBranch from "../../../becca/entities/bbranch.js"; import SearchContext from "../search_context.js"; import dateUtils from "../../date_utils.js"; import becca from "../../../becca/becca.js"; -import becca_mocking from "../../../../spec/support/becca_mocking.js"; +import { findNoteByTitle, note, NoteBuilder } from "../../../../spec/support/becca_mocking.js"; describe("Search", () => { let rootNote: any; @@ -13,7 +13,7 @@ describe("Search", () => { beforeEach(() => { becca.reset(); - rootNote = new becca_mocking.NoteBuilder(new BNote({ noteId: "root", title: "root", type: "text" })); + rootNote = new NoteBuilder(new BNote({ noteId: "root", title: "root", type: "text" })); new BBranch({ branchId: "none_root", noteId: "root", @@ -23,18 +23,18 @@ describe("Search", () => { }); it.skip("simple path match", () => { - rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria"))); + rootNote.child(note("Europe").child(note("Austria"))); const searchContext = new SearchContext(); const searchResults = searchService.findResultsWithQuery("europe austria", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); it.skip("normal search looks also at attributes", () => { - const austria = becca_mocking.note("Austria"); - const vienna = becca_mocking.note("Vienna"); + const austria = note("Austria"); + const vienna = note("Vienna"); rootNote.child(austria.relation("capital", vienna.note)).child(vienna.label("inhabitants", "1888776")); @@ -42,27 +42,27 @@ describe("Search", () => { let searchResults = searchService.findResultsWithQuery("capital", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("inhabitants", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Vienna")).toBeTruthy(); }); it.skip("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" })); + rootNote.child(note("Effective Java", { type: "book", mime: "" })).child(note("Hello World.java", { type: "code", mime: "text/x-java" })); const searchContext = new SearchContext(); let searchResults = searchService.findResultsWithQuery("book", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Effective Java")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Effective Java")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("text", searchContext); // should match mime expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Hello World.java")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Hello World.java")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("java", searchContext); @@ -70,110 +70,104 @@ describe("Search", () => { }); it.skip("only end leafs are results", () => { - rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria"))); + rootNote.child(note("Europe").child(note("Austria"))); const searchContext = new SearchContext(); const searchResults = searchService.findResultsWithQuery("europe", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); }); it.skip("only end leafs are results", () => { - rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna"))); + rootNote.child(note("Europe").child(note("Austria").label("capital", "Vienna"))); const searchContext = new SearchContext(); const searchResults = searchService.findResultsWithQuery("Vienna", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(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"))); + rootNote.child(note("Europe").child(note("Austria").label("capital", "Vienna")).child(note("Czech Republic").label("capital", "Prague"))); const searchContext = new SearchContext(); let searchResults = searchService.findResultsWithQuery("#capital=Vienna", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); // case sensitivity: searchResults = searchService.findResultsWithQuery("#CAPITAL=VIENNA", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("#caPItal=vienNa", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); 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"))); + rootNote.child(note("Europe").child(note("Austria").label("capital", "Vienna")).child(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(); + expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); it("numeric label comparison", () => { - rootNote.child( - becca_mocking - .note("Europe") + rootNote.child(note("Europe") .label("country", "", true) - .child(becca_mocking.note("Austria").label("population", "8859000")) - .child(becca_mocking.note("Czech Republic").label("population", "10650000")) + .child(note("Austria").label("population", "8859000")) + .child(note("Czech Republic").label("population", "10650000")) ); const searchContext = new SearchContext(); const searchResults = searchService.findResultsWithQuery("#country #population >= 10000000", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); it.skip("inherited label comparison", () => { - rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic"))); + rootNote.child(note("Europe").label("country", "", true).child(note("Austria")).child(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(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); 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") + rootNote.child(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")) + .child(note("Austria").label("established", "1955-07-27")) + .child(note("Czech Republic").label("established", "1993-01-01")) + .child(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(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(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); 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() }) + rootNote.child(note("My note", { dateCreated: dateUtils.localNowDateTime() }) .label("year", new Date().getFullYear().toString()) .label("month", dateUtils.localNowDate().substr(0, 7)) .label("date", dateUtils.localNowDate()) @@ -188,7 +182,7 @@ describe("Search", () => { .toEqual(expectedResultCount); if (expectedResultCount === 1) { - expect(becca_mocking.findNoteByTitle(searchResults, "My note")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "My note")).toBeTruthy(); } } @@ -225,30 +219,26 @@ describe("Search", () => { }); it("logical or", () => { - rootNote.child( - becca_mocking - .note("Europe") + rootNote.child(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")) + .child(note("Austria").label("languageFamily", "germanic")) + .child(note("Czech Republic").label("languageFamily", "slavic")) + .child(note("Hungary").label("languageFamily", "finnougric")) ); const searchContext = new 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(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); it("fuzzy attribute search", () => { - rootNote.child( - becca_mocking - .note("Europe") + rootNote.child(note("Europe") .label("country", "", true) - .child(becca_mocking.note("Austria").label("languageFamily", "germanic")) - .child(becca_mocking.note("Czech Republic").label("languageFamily", "slavic")) + .child(note("Austria").label("languageFamily", "germanic")) + .child(note("Czech Republic").label("languageFamily", "slavic")) ); let searchContext = new SearchContext({ fuzzyAttributeSearch: false }); @@ -266,147 +256,135 @@ describe("Search", () => { searchResults = searchService.findResultsWithQuery("#languageFamily=ger", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(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"))); + rootNote.child(note("Europe").child(note("Austria")).child(note("Czech Republic"))); const searchContext = new SearchContext(); const searchResults = searchService.findResultsWithQuery("# note.title =* czech", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + expect(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"))) + .child(note("Europe") + .child(note("Austria")) + .child(note("Czech Republic").child(note("Prague"))) ) - .child(becca_mocking.note("Asia").child(becca_mocking.note("Taiwan"))); + .child(note("Asia").child(note("Taiwan"))); const searchContext = new 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(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("# note.parents.title = Asia", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Taiwan")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Taiwan")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("# note.parents.parents.title = Europe", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Prague")).toBeTruthy(); + expect(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"))) + .child(note("Europe") + .child(note("Austria")) + .child(note("Czech Republic").child(note("Prague").label("city"))) ) - .child(becca_mocking.note("Asia").child(becca_mocking.note("Taiwan").child(becca_mocking.note("Taipei").label("city")))); + .child(note("Asia").child(note("Taiwan").child(note("Taipei").label("city")))); const searchContext = new 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(findNoteByTitle(searchResults, "Prague")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("#city AND note.ancestors.title = Asia", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Taipei")).toBeTruthy(); + expect(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"))) + .child(note("Europe") + .child(note("Austria").child(note("Vienna"))) + .child(note("Czech Republic").child(note("Prague"))) ) - .child(becca_mocking.note("Oceania").child(becca_mocking.note("Australia"))); + .child(note("Oceania").child(note("Australia"))); const searchContext = new 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(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Oceania")).toBeTruthy(); 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(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("# note.children.children.title = Prague", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy(); + expect(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 = note("Austria"); + const portugal = note("Portugal"); - rootNote.child( - becca_mocking - .note("Europe") + rootNote.child(note("Europe") .child(austria) - .child(becca_mocking.note("Czech Republic").relation("neighbor", austria.note)) + .child(note("Czech Republic").relation("neighbor", austria.note)) .child(portugal) - .child(becca_mocking.note("Spain").relation("neighbor", portugal.note)) + .child(note("Spain").relation("neighbor", portugal.note)) ); const searchContext = new SearchContext(); let searchResults = searchService.findResultsWithQuery("# ~neighbor.title = Austria", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); searchResults = searchService.findResultsWithQuery("# ~neighbor.title = Portugal", searchContext); expect(searchResults.length).toEqual(1); - expect(becca_mocking.findNoteByTitle(searchResults, "Spain")).toBeTruthy(); + expect(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 = note("Austria"); + const portugal = note("Portugal"); - rootNote.child( - becca_mocking - .note("Europe") + rootNote.child(note("Europe") .child(austria) - .child(becca_mocking.note("Czech Republic").relation("neighbor", austria.note)) + .child(note("Czech Republic").relation("neighbor", austria.note)) .child(portugal) - .child(becca_mocking.note("Spain").relation("neighbor", portugal.note)) + .child(note("Spain").relation("neighbor", portugal.note)) ); const searchContext = new 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(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"); + const austria = note("Austria"); + const slovakia = note("Slovakia"); + const italy = note("Italy"); + const ukraine = note("Ukraine"); - rootNote.child( - becca_mocking - .note("Europe") + rootNote.child(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(note("Czech Republic").relation("neighbor", austria.note).relation("neighbor", slovakia.note)) .child(slovakia.relation("neighbor", ukraine.note)) .child(ukraine) ); @@ -415,25 +393,25 @@ describe("Search", () => { 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(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); 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(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); it("test note properties", () => { - const austria = becca_mocking.note("Austria"); + const austria = note("Austria"); 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(note("Asia")) + .child(note("Europe").child(austria.child(note("Vienna")).child(note("Sebastian Kurz")))) + .child(note("Mozart").child(austria)); austria.note.isProtected = false; austria.note.dateCreated = "2020-05-14 12:11:42.001+0200"; @@ -490,12 +468,12 @@ describe("Search", () => { }); 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"); + const italy = note("Italy").label("capital", "Rome"); + const slovakia = note("Slovakia").label("capital", "Bratislava"); + const austria = note("Austria").label("capital", "Vienna"); + const ukraine = note("Ukraine").label("capital", "Kiev"); - rootNote.child(becca_mocking.note("Europe").child(ukraine).child(slovakia).child(austria).child(italy)); + rootNote.child(note("Europe").child(ukraine).child(slovakia).child(austria).child(italy)); const searchContext = new SearchContext(); @@ -533,10 +511,10 @@ describe("Search", () => { }); it("test not(...)", () => { - const italy = becca_mocking.note("Italy").label("capital", "Rome"); - const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava"); + const italy = note("Italy").label("capital", "Rome"); + const slovakia = note("Slovakia").label("capital", "Bratislava"); - rootNote.child(becca_mocking.note("Europe").child(slovakia).child(italy)); + rootNote.child(note("Europe").child(slovakia).child(italy)); const searchContext = new SearchContext(); @@ -550,10 +528,10 @@ describe("Search", () => { }); it.skip("test note.text *=* something", () => { - const italy = becca_mocking.note("Italy").label("capital", "Rome"); - const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava"); + const italy = note("Italy").label("capital", "Rome"); + const slovakia = note("Slovakia").label("capital", "Bratislava"); - rootNote.child(becca_mocking.note("Europe").child(slovakia).child(italy)); + rootNote.child(note("Europe").child(slovakia).child(italy)); const searchContext = new SearchContext(); @@ -563,10 +541,10 @@ describe("Search", () => { }); it.skip("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"); + const italy = note("Italy").label("capital", "Rome"); + const slovakia = 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(note("Reddit").label("archived", "", true).child(note("Post X")).child(note("Post Y"))).child(note("Reddit is bad")); const searchContext = new SearchContext({ includeArchivedNotes: false }); @@ -579,14 +557,14 @@ describe("Search", () => { // it("comparison between labels", () => { // rootNote - // .child(becca_mocking.note("Europe") - // .child(becca_mocking.note("Austria") + // .child(note("Europe") + // .child(note("Austria") // .label('capital', 'Vienna') // .label('largestCity', 'Vienna')) - // .child(becca_mocking.note("Canada") + // .child(note("Canada") // .label('capital', 'Ottawa') // .label('largestCity', 'Toronto')) - // .child(becca_mocking.note("Czech Republic") + // .child(note("Czech Republic") // .label('capital', 'Prague') // .label('largestCity', 'Prague')) // ); @@ -595,7 +573,7 @@ describe("Search", () => { // // const searchResults = searchService.findResultsWithQuery('#capital = #largestCity', searchContext); // expect(searchResults.length).toEqual(2); - // expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - // expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + // expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + // expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); // }) }); diff --git a/src/services/search/value_extractor.spec.ts b/src/services/search/value_extractor.spec.ts index f7de5a281..903df6162 100644 --- a/src/services/search/value_extractor.spec.ts +++ b/src/services/search/value_extractor.spec.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach } from "vitest"; -import becca_mocking from "../../../spec/support/becca_mocking.js"; import ValueExtractor from "./value_extractor.js"; import becca from "../../becca/becca.js"; import SearchContext from "./search_context.js"; +import { note } from "../../../spec/support/becca_mocking.js"; const dsc = new SearchContext(); @@ -12,7 +12,7 @@ describe("Value extractor", () => { }); it("simple title extraction", async () => { - const europe = becca_mocking.note("Europe").note; + const europe = note("Europe").note; const valueExtractor = new ValueExtractor(dsc, ["note", "title"]); @@ -21,7 +21,7 @@ describe("Value extractor", () => { }); it("label extraction", async () => { - const austria = becca_mocking.note("Austria").label("Capital", "Vienna").note; + const austria = note("Austria").label("Capital", "Vienna").note; let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]); @@ -35,8 +35,8 @@ describe("Value extractor", () => { }); 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)); + const vienna = note("Vienna"); + const europe = note("Europe").child(note("Austria").child(vienna)); let valueExtractor = new ValueExtractor(dsc, ["note", "children", "children", "title"]); @@ -50,9 +50,9 @@ describe("Value extractor", () => { }); 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); + const czechRepublic = note("Czech Republic").label("capital", "Prague"); + const slovakia = note("Slovakia").label("capital", "Bratislava"); + const austria = note("Austria").relation("neighbor", czechRepublic.note).relation("neighbor", slovakia.note); let valueExtractor = new ValueExtractor(dsc, ["note", "relations", "neighbor", "labels", "capital"]); diff --git a/src/services/tree.spec.ts b/src/services/tree.spec.ts new file mode 100644 index 000000000..0c519fc21 --- /dev/null +++ b/src/services/tree.spec.ts @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { note, NoteBuilder } from "../../spec/support/becca_mocking.js"; +import becca from "../becca/becca.js"; +import BBranch from "../becca/entities/bbranch.js"; +import BNote from "../becca/entities/bnote.js"; +import tree from "./tree.js"; +import cls from "./cls.js"; + +describe("Tree", () => { + let rootNote!: NoteBuilder; + + beforeEach(() => { + becca.reset(); + + rootNote = new NoteBuilder(new BNote({ + noteId: "root", + title: "root", + type: "text" + })) + new BBranch({ + branchId: "none_root", + noteId: "root", + parentNoteId: "none", + notePosition: 10 + }); + + vi.mock("./sql.js", () => { + return { + default: { + transactional: (cb: Function) => { + cb(); + }, + execute: () => { }, + replace: () => { }, + getMap: () => { } + } + } + }); + + vi.mock("./sql_init.js", () => { + return { + dbReady: () => { console.log("Hello world") } + } + }); + }); + + it("custom sort order is idempotent", () => { + rootNote.label("sorted", "order"); + + // Add values which have a defined order. + for (let i=0; i<=5; i++) { + rootNote.child(note(String(i)).label("order", String(i))); + } + + // Add a few values which have no defined order. + for (let i=6; i<10; i++) { + rootNote.child(note(String(i))); + } + + const expectedOrder = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ]; + + // Sort a few times to ensure that the resulting order is the same. + for (let i=0; i<5; i++) { + cls.init(() => { + tree.sortNotesIfNeeded(rootNote.note.noteId); + }); + + const order = rootNote.note.children.map((child) => child.title); + expect(order).toStrictEqual(expectedOrder); + } + }); +}); diff --git a/src/services/tree.ts b/src/services/tree.ts index 64d1279e5..9b695cd69 100644 --- a/src/services/tree.ts +++ b/src/services/tree.ts @@ -145,8 +145,8 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse return compare(bottomBEl, bottomAEl) * (reverse ? -1 : 1); } - const customAEl = fetchValue(a, customSortBy); - const customBEl = fetchValue(b, customSortBy); + const customAEl = fetchValue(a, customSortBy) ?? fetchValue(a, "title"); + const customBEl = fetchValue(b, customSortBy) ?? fetchValue(b, "title"); if (customAEl !== customBEl) { return compare(customAEl, customBEl);