Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,7 +1,6 @@ | |||||||
| name: Bug Report | name: Bug Report | ||||||
| description: Report a bug | description: Report a bug | ||||||
| title: "(Bug report) " | type: "Bug" | ||||||
| labels: "Type: Bug" |  | ||||||
| body: | body: | ||||||
| - type: textarea | - type: textarea | ||||||
|   attributes: |   attributes: | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/feature_request.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,12 +1,11 @@ | |||||||
| name: Feature Request | name: Feature Request | ||||||
| description: Ask for a new feature to be added | description: Ask for a new feature to be added | ||||||
| title: "(Feature request) " | type: "Feature" | ||||||
| labels: "Type: Enhancement" |  | ||||||
| body: | body: | ||||||
| - type: textarea | - type: textarea | ||||||
|   attributes: |   attributes: | ||||||
|     label: Describe feature |     label: Describe feature | ||||||
|     description: A clear and concise description of what you want to be added.. |     description: A clear and concise description of what you want to be added. | ||||||
|   validations: |   validations: | ||||||
|     required: true |     required: true | ||||||
| - type: textarea | - type: textarea | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -92,7 +92,16 @@ jobs: | |||||||
|           asset_content_type: application/zip # required by GitHub API |           asset_content_type: application/zip # required by GitHub API | ||||||
|   nightly-server: |   nightly-server: | ||||||
|     name: Deploy server nightly |     name: Deploy server nightly | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         arch: [x64, arm64] | ||||||
|  |         include: | ||||||
|  |           - arch: x64 | ||||||
|             runs-on: ubuntu-latest |             runs-on: ubuntu-latest | ||||||
|  |           - arch: arm64 | ||||||
|  |             runs-on: ubuntu-24.04-arm | ||||||
|  |     runs-on: ${{ matrix.runs-on }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - name: Set up node & dependencies |       - name: Set up node & dependencies | ||||||
| @ -102,22 +111,21 @@ jobs: | |||||||
|           cache: "npm" |           cache: "npm" | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: npm ci |         run: npm ci | ||||||
|       - name: Run Linux server build (x86_64) |       - name: Run Linux server build | ||||||
|  |         env: | ||||||
|  |           MATRIX_ARCH: ${{ matrix.arch }} | ||||||
|         run: | |         run: | | ||||||
|           npm run update-build-info |           npm run update-build-info | ||||||
|           npm run ci-update-nightly-version |  | ||||||
|           ./bin/build-server.sh |           ./bin/build-server.sh | ||||||
|       - name: Prepare artifacts |       - name: Prepare artifacts | ||||||
|         if: runner.os != 'windows' |  | ||||||
|         run: | |         run: | | ||||||
|           mkdir -p upload |           mkdir -p upload | ||||||
|           file=$(find dist -name '*.tar.xz' -print -quit) |           file=$(find dist -name '*.tar.xz' -print -quit) | ||||||
|           cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz" |           cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz" | ||||||
|       - uses: actions/upload-artifact@v4 |       - uses: actions/upload-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           name: TriliumNextNotes linux server x64 |           name: TriliumNextNotes linux server ${{ matrix.arch }} | ||||||
|           path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz |           path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz | ||||||
|           overwrite: true |  | ||||||
| 
 | 
 | ||||||
|       - name: Deploy release |       - name: Deploy release | ||||||
|         uses: WebFreak001/deploy-nightly@v3.2.0 |         uses: WebFreak001/deploy-nightly@v3.2.0 | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -66,8 +66,17 @@ jobs: | |||||||
|           fail_on_unmatched_files: true |           fail_on_unmatched_files: true | ||||||
|           files: upload/*.* |           files: upload/*.* | ||||||
|   build_linux_server-x64: |   build_linux_server-x64: | ||||||
|     name: Build Linux Server x86_64 |     name: Build Linux Server | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         arch: [x64, arm64] | ||||||
|  |         include: | ||||||
|  |           - arch: x64 | ||||||
|             runs-on: ubuntu-latest |             runs-on: ubuntu-latest | ||||||
|  |           - arch: arm64 | ||||||
|  |             runs-on: ubuntu-24.04-arm | ||||||
|  |     runs-on: ${{ matrix.runs-on }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|       - name: Set up node & dependencies |       - name: Set up node & dependencies | ||||||
| @ -77,16 +86,17 @@ jobs: | |||||||
|           cache: "npm" |           cache: "npm" | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: npm ci |         run: npm ci | ||||||
|       - name: Run Linux server build (x86_64) |       - name: Run Linux server build | ||||||
|  |         env: | ||||||
|  |           MATRIX_ARCH: ${{ matrix.arch }} | ||||||
|         run: | |         run: | | ||||||
|           npm run update-build-info |           npm run update-build-info | ||||||
|           ./bin/build-server.sh |           ./bin/build-server.sh | ||||||
|       - name: Prepare artifacts |       - name: Prepare artifacts | ||||||
|         if: runner.os != 'windows' |  | ||||||
|         run: | |         run: | | ||||||
|           mkdir -p upload |           mkdir -p upload | ||||||
|           file=$(find dist -name '*.tar.xz' -print -quit) |           file=$(find dist -name '*.tar.xz' -print -quit) | ||||||
|           cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-server-linux-x64.tar.xz" |           cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz" | ||||||
|       - name: Publish release |       - name: Publish release | ||||||
|         uses: softprops/action-gh-release@v2 |         uses: softprops/action-gh-release@v2 | ||||||
|         with: |         with: | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| # Build stage | # Build stage | ||||||
| FROM node:22.13.0-bullseye-slim AS builder | FROM node:22.13.1-bullseye-slim AS builder | ||||||
| 
 | 
 | ||||||
| # Configure build dependencies in a single layer | # Configure build dependencies in a single layer | ||||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||||
| @ -28,7 +28,6 @@ RUN cp -R build/src/* src/. && \ | |||||||
|     npm run webpack && \ |     npm run webpack && \ | ||||||
|     npm prune --omit=dev && \ |     npm prune --omit=dev && \ | ||||||
|     npm cache clean --force && \ |     npm cache clean --force && \ | ||||||
|     cp src/public/app/share.js src/public/app-dist/. && \ |  | ||||||
|     cp -r src/public/app/doc_notes src/public/app-dist/. && \ |     cp -r src/public/app/doc_notes src/public/app-dist/. && \ | ||||||
|     rm -rf src/public/app/* && \ |     rm -rf src/public/app/* && \ | ||||||
|     mkdir -p src/public/app/services && \ |     mkdir -p src/public/app/services && \ | ||||||
| @ -37,7 +36,7 @@ RUN cp -R build/src/* src/. && \ | |||||||
|     rm -r build |     rm -r build | ||||||
| 
 | 
 | ||||||
| # Runtime stage | # Runtime stage | ||||||
| FROM node:22.13.0-bullseye-slim | FROM node:22.13.1-bullseye-slim | ||||||
| 
 | 
 | ||||||
| # Install only runtime dependencies | # Install only runtime dependencies | ||||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| # Build stage | # Build stage | ||||||
| FROM node:22.13.0-alpine AS builder | FROM node:22.13.1-alpine AS builder | ||||||
| 
 | 
 | ||||||
| # Configure build dependencies | # Configure build dependencies | ||||||
| RUN apk add --no-cache --virtual .build-dependencies \ | RUN apk add --no-cache --virtual .build-dependencies \ | ||||||
| @ -27,7 +27,6 @@ RUN cp -R build/src/* src/. && \ | |||||||
|     npm run webpack && \ |     npm run webpack && \ | ||||||
|     npm prune --omit=dev && \ |     npm prune --omit=dev && \ | ||||||
|     npm cache clean --force && \ |     npm cache clean --force && \ | ||||||
|     cp src/public/app/share.js src/public/app-dist/. && \ |  | ||||||
|     cp -r src/public/app/doc_notes src/public/app-dist/. && \ |     cp -r src/public/app/doc_notes src/public/app-dist/. && \ | ||||||
|     rm -rf src/public/app && \ |     rm -rf src/public/app && \ | ||||||
|     mkdir -p src/public/app/services && \ |     mkdir -p src/public/app/services && \ | ||||||
| @ -36,7 +35,7 @@ RUN cp -R build/src/* src/. && \ | |||||||
|     rm -r build |     rm -r build | ||||||
| 
 | 
 | ||||||
| # Runtime stage | # Runtime stage | ||||||
| FROM node:22.13.0-alpine | FROM node:22.13.1-alpine | ||||||
| 
 | 
 | ||||||
| # Install runtime dependencies | # Install runtime dependencies | ||||||
| RUN apk add --no-cache su-exec shadow | RUN apk add --no-cache su-exec shadow | ||||||
|  | |||||||
| @ -68,7 +68,6 @@ find $DIR -name "*.ts" -type f -delete | |||||||
| 
 | 
 | ||||||
| d="$DIR"/src/public | d="$DIR"/src/public | ||||||
| [[ -d "$d"/app-dist ]] || mkdir -pv "$d"/app-dist | [[ -d "$d"/app-dist ]] || mkdir -pv "$d"/app-dist | ||||||
| cp "$d"/app/share.js "$d"/app-dist/ |  | ||||||
| cp -r "$d"/app/doc_notes "$d"/app-dist/ | cp -r "$d"/app/doc_notes "$d"/app-dist/ | ||||||
| 
 | 
 | ||||||
| rm -rf "$d"/app | rm -rf "$d"/app | ||||||
|  | |||||||
| @ -27,3 +27,8 @@ keyPath= | |||||||
| # once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients. | # once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients. | ||||||
| # expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) | # expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) | ||||||
| trustedReverseProxy=false | trustedReverseProxy=false | ||||||
|  | 
 | ||||||
|  | [Sync] | ||||||
|  | #syncServerHost= | ||||||
|  | #syncServerTimeout= | ||||||
|  | #syncServerProxy= | ||||||
| Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.4 KiB | 
| @ -1,8 +1,5 @@ | |||||||
| import http from "http"; | import http from "http"; | ||||||
| import ini from "ini"; | import config from "./src/services/config.js"; | ||||||
| import fs from "fs"; |  | ||||||
| import dataDir from "./src/services/data_dir.js"; |  | ||||||
| const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8")); |  | ||||||
| 
 | 
 | ||||||
| if (config.Network.https) { | if (config.Network.https) { | ||||||
|     // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
 |     // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
 | ||||||
|  | |||||||
| Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 3.9 KiB | 
| Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 4.1 KiB | 
| Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 32 KiB | 
| Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 3.2 KiB | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.6 KiB | 
| Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.5 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.4 KiB | 
| @ -1,5 +0,0 @@ | |||||||
| For bug reports, **PLEASE mention version of Trilium you're using** and also include **log files** from following location: |  | ||||||
| 
 |  | ||||||
| * `/home/[user]/.local/share/trilium-data/log` for Linux |  | ||||||
| * `C:\Users\[user]\AppData\Roaming\trilium-data\log` for Windows Vista and up |  | ||||||
| * `/Users/[user]/Library/Application Support/trilium-data/log` for Mac OS |  | ||||||
							
								
								
									
										734
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										36
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @ -2,7 +2,7 @@ | |||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "productName": "TriliumNext Notes", |   "productName": "TriliumNext Notes", | ||||||
|   "description": "Build your personal knowledge base with TriliumNext Notes", |   "description": "Build your personal knowledge base with TriliumNext Notes", | ||||||
|   "version": "0.91.2-beta", |   "version": "0.91.4-beta", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "./dist/electron-main.js", |   "main": "./dist/electron-main.js", | ||||||
|   "author": { |   "author": { | ||||||
| @ -26,9 +26,9 @@ | |||||||
|     "start-test-server": "npm run switch-server && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts", |     "start-test-server": "npm run switch-server && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts", | ||||||
|     "qstart-server": "npm run switch-server && npm run start-server", |     "qstart-server": "npm run switch-server && npm run start-server", | ||||||
|     "start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .", |     "start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .", | ||||||
|     "start-electron-nix": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"", |     "start-electron-nix": "electron-rebuild --version 33.3.1 && npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"", | ||||||
|     "start-electron-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --inspect=5858 .", |     "start-electron-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --inspect=5858 .", | ||||||
|     "start-electron-no-dir-nix": "npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"", |     "start-electron-no-dir-nix": "electron-rebuild --version 33.3.1 && npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"", | ||||||
|     "qstart-electron": "npm run switch-electron && npm run start-electron", |     "qstart-electron": "npm run switch-electron && npm run start-electron", | ||||||
|     "switch-server": "rimraf ./node_modules/better-sqlite3 && npm install", |     "switch-server": "rimraf ./node_modules/better-sqlite3 && npm install", | ||||||
|     "switch-electron": "electron-rebuild", |     "switch-electron": "electron-rebuild", | ||||||
| @ -59,7 +59,7 @@ | |||||||
|     "@excalidraw/excalidraw": "0.17.6", |     "@excalidraw/excalidraw": "0.17.6", | ||||||
|     "@highlightjs/cdn-assets": "11.11.1", |     "@highlightjs/cdn-assets": "11.11.1", | ||||||
|     "@mermaid-js/layout-elk": "0.1.7", |     "@mermaid-js/layout-elk": "0.1.7", | ||||||
|     "@mind-elixir/node-menu": "1.0.3", |     "@mind-elixir/node-menu": "1.0.4", | ||||||
|     "@triliumnext/express-partial-content": "1.0.1", |     "@triliumnext/express-partial-content": "1.0.1", | ||||||
|     "@types/leaflet": "1.9.16", |     "@types/leaflet": "1.9.16", | ||||||
|     "@types/react-dom": "18.3.5", |     "@types/react-dom": "18.3.5", | ||||||
| @ -97,9 +97,9 @@ | |||||||
|     "html2plaintext": "2.1.4", |     "html2plaintext": "2.1.4", | ||||||
|     "http-proxy-agent": "7.0.2", |     "http-proxy-agent": "7.0.2", | ||||||
|     "https-proxy-agent": "7.0.6", |     "https-proxy-agent": "7.0.6", | ||||||
|     "i18next": "24.2.1", |     "i18next": "24.2.2", | ||||||
|     "i18next-fs-backend": "2.6.0", |     "i18next-fs-backend": "2.6.0", | ||||||
|     "i18next-http-backend": "3.0.1", |     "i18next-http-backend": "3.0.2", | ||||||
|     "image-type": "5.2.0", |     "image-type": "5.2.0", | ||||||
|     "ini": "5.0.0", |     "ini": "5.0.0", | ||||||
|     "is-animated": "2.0.2", |     "is-animated": "2.0.2", | ||||||
| @ -148,14 +148,14 @@ | |||||||
|     "yauzl": "3.2.0" |     "yauzl": "3.2.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@electron-forge/cli": "7.6.0", |     "@electron-forge/cli": "7.6.1", | ||||||
|     "@electron-forge/maker-deb": "7.6.0", |     "@electron-forge/maker-deb": "7.6.1", | ||||||
|     "@electron-forge/maker-dmg": "7.6.0", |     "@electron-forge/maker-dmg": "7.6.1", | ||||||
|     "@electron-forge/maker-squirrel": "7.6.0", |     "@electron-forge/maker-squirrel": "7.6.1", | ||||||
|     "@electron-forge/maker-zip": "7.6.0", |     "@electron-forge/maker-zip": "7.6.1", | ||||||
|     "@electron-forge/plugin-auto-unpack-natives": "7.6.0", |     "@electron-forge/plugin-auto-unpack-natives": "7.6.1", | ||||||
|     "@electron/rebuild": "3.7.1", |     "@electron/rebuild": "3.7.1", | ||||||
|     "@playwright/test": "1.49.1", |     "@playwright/test": "1.50.0", | ||||||
|     "@types/archiver": "6.0.3", |     "@types/archiver": "6.0.3", | ||||||
|     "@types/better-sqlite3": "7.6.12", |     "@types/better-sqlite3": "7.6.12", | ||||||
|     "@types/bootstrap": "5.2.10", |     "@types/bootstrap": "5.2.10", | ||||||
| @ -177,7 +177,7 @@ | |||||||
|     "@types/jsdom": "21.1.7", |     "@types/jsdom": "21.1.7", | ||||||
|     "@types/mime-types": "2.1.4", |     "@types/mime-types": "2.1.4", | ||||||
|     "@types/multer": "1.4.12", |     "@types/multer": "1.4.12", | ||||||
|     "@types/node": "22.10.7", |     "@types/node": "22.12.0", | ||||||
|     "@types/react": "18.3.18", |     "@types/react": "18.3.18", | ||||||
|     "@types/safe-compare": "1.1.2", |     "@types/safe-compare": "1.1.2", | ||||||
|     "@types/sanitize-html": "2.13.0", |     "@types/sanitize-html": "2.13.0", | ||||||
| @ -189,12 +189,12 @@ | |||||||
|     "@types/stream-throttle": "0.1.4", |     "@types/stream-throttle": "0.1.4", | ||||||
|     "@types/tmp": "0.2.6", |     "@types/tmp": "0.2.6", | ||||||
|     "@types/turndown": "5.0.5", |     "@types/turndown": "5.0.5", | ||||||
|     "@types/ws": "8.5.13", |     "@types/ws": "8.5.14", | ||||||
|     "@types/xml2js": "0.4.14", |     "@types/xml2js": "0.4.14", | ||||||
|     "@types/yargs": "17.0.33", |     "@types/yargs": "17.0.33", | ||||||
|     "@vitest/coverage-v8": "3.0.3", |     "@vitest/coverage-v8": "3.0.4", | ||||||
|     "cross-env": "7.0.3", |     "cross-env": "7.0.3", | ||||||
|     "electron": "34.0.0", |     "electron": "34.0.1", | ||||||
|     "esm": "3.2.25", |     "esm": "3.2.25", | ||||||
|     "jasmine": "5.5.0", |     "jasmine": "5.5.0", | ||||||
|     "jsdoc": "4.0.4", |     "jsdoc": "4.0.4", | ||||||
| @ -207,7 +207,7 @@ | |||||||
|     "tsx": "4.19.2", |     "tsx": "4.19.2", | ||||||
|     "typedoc": "0.27.6", |     "typedoc": "0.27.6", | ||||||
|     "typescript": "5.7.3", |     "typescript": "5.7.3", | ||||||
|     "vitest": "3.0.3", |     "vitest": "3.0.4", | ||||||
|     "webpack": "5.97.1", |     "webpack": "5.97.1", | ||||||
|     "webpack-cli": "6.0.1", |     "webpack-cli": "6.0.1", | ||||||
|     "webpack-dev-middleware": "7.4.2" |     "webpack-dev-middleware": "7.4.2" | ||||||
|  | |||||||
| @ -36,6 +36,12 @@ interface DateLimits { | |||||||
|     maxDate: string; |     maxDate: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | interface SimilarNote { | ||||||
|  |     score: number; | ||||||
|  |     notePath: string[]; | ||||||
|  |     noteId: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function filterUrlValue(value: string) { | function filterUrlValue(value: string) { | ||||||
|     return value |     return value | ||||||
|         .replace(/https?:\/\//gi, "") |         .replace(/https?:\/\//gi, "") | ||||||
| @ -247,7 +253,7 @@ 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) { | async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefined> { | ||||||
|     const results = []; |     const results = []; | ||||||
|     let i = 0; |     let i = 0; | ||||||
| 
 | 
 | ||||||
| @ -417,6 +423,7 @@ async function findSimilarNotes(noteId: string) { | |||||||
| 
 | 
 | ||||||
|             // this takes care of note hoisting
 |             // this takes care of note hoisting
 | ||||||
|             if (!notePath) { |             if (!notePath) { | ||||||
|  |                 // TODO: This return is suspicious, it should probably be continue
 | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -71,7 +71,7 @@ export interface ExecuteCommandData extends CommandData { | |||||||
| export type CommandMappings = { | export type CommandMappings = { | ||||||
|     "api-log-messages": CommandData; |     "api-log-messages": CommandData; | ||||||
|     focusTree: CommandData, |     focusTree: CommandData, | ||||||
|     focusOnDetail: Required<CommandData>; |     focusOnDetail: CommandData; | ||||||
|     focusOnSearchDefinition: Required<CommandData>; |     focusOnSearchDefinition: Required<CommandData>; | ||||||
|     searchNotes: CommandData & { |     searchNotes: CommandData & { | ||||||
|         searchString?: string; |         searchString?: string; | ||||||
| @ -104,6 +104,8 @@ export type CommandMappings = { | |||||||
|     openNoteInNewTab: CommandData; |     openNoteInNewTab: CommandData; | ||||||
|     openNoteInNewSplit: CommandData; |     openNoteInNewSplit: CommandData; | ||||||
|     openNoteInNewWindow: CommandData; |     openNoteInNewWindow: CommandData; | ||||||
|  |     hideLeftPane: CommandData; | ||||||
|  |     showLeftPane: CommandData; | ||||||
| 
 | 
 | ||||||
|     openInTab: ContextMenuCommandData; |     openInTab: ContextMenuCommandData; | ||||||
|     openNoteInSplit: ContextMenuCommandData; |     openNoteInSplit: ContextMenuCommandData; | ||||||
| @ -236,6 +238,9 @@ type EventMappings = { | |||||||
|     beforeNoteSwitch: { |     beforeNoteSwitch: { | ||||||
|         noteContext: NoteContext; |         noteContext: NoteContext; | ||||||
|     }; |     }; | ||||||
|  |     beforeNoteContextRemove: { | ||||||
|  |         ntxIds: string[]; | ||||||
|  |     }; | ||||||
|     noteSwitched: { |     noteSwitched: { | ||||||
|         noteContext: NoteContext; |         noteContext: NoteContext; | ||||||
|         notePath: string | null; |         notePath: string | null; | ||||||
| @ -286,6 +291,9 @@ type EventMappings = { | |||||||
|     tabReorder: { |     tabReorder: { | ||||||
|         ntxIdsInOrder: string[] |         ntxIdsInOrder: string[] | ||||||
|     }; |     }; | ||||||
|  |     refreshNoteList: { | ||||||
|  |         noteId: string; | ||||||
|  |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type EventListener<T extends EventNames> = { | export type EventListener<T extends EventNames> = { | ||||||
|  | |||||||
| @ -46,7 +46,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> { | |||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null { |     handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined { | ||||||
|         try { |         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); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -9,6 +9,8 @@ import electronContextMenu from "./menus/electron_context_menu.js"; | |||||||
| import glob from "./services/glob.js"; | import glob from "./services/glob.js"; | ||||||
| import { t } from "./services/i18n.js"; | import { t } from "./services/i18n.js"; | ||||||
| import options from "./services/options.js"; | import options from "./services/options.js"; | ||||||
|  | import type ElectronRemote from "@electron/remote"; | ||||||
|  | import type Electron from "electron"; | ||||||
| 
 | 
 | ||||||
| await appContext.earlyInit(); | await appContext.earlyInit(); | ||||||
| 
 | 
 | ||||||
| @ -44,10 +46,9 @@ if (utils.isElectron()) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function initOnElectron() { | function initOnElectron() { | ||||||
|     const electron = utils.dynamicRequire("electron"); |     const electron: typeof Electron = utils.dynamicRequire("electron"); | ||||||
|     electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName)); |     electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName)); | ||||||
| 
 |     const electronRemote: typeof ElectronRemote = utils.dynamicRequire("@electron/remote"); | ||||||
|     const electronRemote = utils.dynamicRequire("@electron/remote"); |  | ||||||
|     const currentWindow = electronRemote.getCurrentWindow(); |     const currentWindow = electronRemote.getCurrentWindow(); | ||||||
|     const style = window.getComputedStyle(document.body); |     const style = window.getComputedStyle(document.body); | ||||||
| 
 | 
 | ||||||
| @ -58,7 +59,7 @@ function initOnElectron() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function initTitleBarButtons(style, currentWindow) { | function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) { | ||||||
|     if (window.glob.platform === "win32") { |     if (window.glob.platform === "win32") { | ||||||
|         const applyWindowsOverlay = () => { |         const applyWindowsOverlay = () => { | ||||||
|             const color = style.getPropertyValue("--native-titlebar-background"); |             const color = style.getPropertyValue("--native-titlebar-background"); | ||||||
| @ -81,9 +82,14 @@ function initTitleBarButtons(style, currentWindow) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function initTransparencyEffects(style, currentWindow) { | function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) { | ||||||
|     if (window.glob.platform === "win32") { |     if (window.glob.platform === "win32") { | ||||||
|         const material = style.getPropertyValue("--background-material"); |         const material = style.getPropertyValue("--background-material"); | ||||||
|         currentWindow.setBackgroundMaterial(material); |         // TriliumNextTODO: find a nicer way to make TypeScript happy – unfortunately TS did not like Array.includes here
 | ||||||
|  |         const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const; | ||||||
|  |         const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption); | ||||||
|  |         if (foundBgMaterialOption) { | ||||||
|  |             currentWindow.setBackgroundMaterial(foundBgMaterialOption); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -36,12 +36,12 @@ const NOTE_TYPE_ICONS = { | |||||||
|  * end user. Those types should be used only for checking against, they are |  * end user. Those types should be used only for checking against, they are | ||||||
|  * not for direct use. |  * not for direct use. | ||||||
|  */ |  */ | ||||||
| type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap"; | export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap"; | ||||||
| 
 | 
 | ||||||
| interface NotePathRecord { | export interface NotePathRecord { | ||||||
|     isArchived: boolean; |     isArchived: boolean; | ||||||
|     isInHoistedSubTree: boolean; |     isInHoistedSubTree: boolean; | ||||||
|     isSearch: boolean; |     isSearch?: boolean; | ||||||
|     notePath: string[]; |     notePath: string[]; | ||||||
|     isHidden: boolean; |     isHidden: boolean; | ||||||
| } | } | ||||||
| @ -402,14 +402,14 @@ class FNote { | |||||||
|         return notePaths; |         return notePaths; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     getSortedNotePathRecords(hoistedNoteId = "root") { |     getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] { | ||||||
|         const isHoistedRoot = hoistedNoteId === "root"; |         const isHoistedRoot = hoistedNoteId === "root"; | ||||||
| 
 | 
 | ||||||
|         const notePaths = this.getAllNotePaths().map((path) => ({ |         const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ | ||||||
|             notePath: path, |             notePath: path, | ||||||
|             isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), |             isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), | ||||||
|             isArchived: path.some((noteId) => froca.notes[noteId].isArchived), |             isArchived: path.some((noteId) => froca.notes[noteId].isArchived), | ||||||
|             isSearch: path.find((noteId) => froca.notes[noteId].type === "search"), |             isSearch: path.some((noteId) => froca.notes[noteId].type === "search"), | ||||||
|             isHidden: path.includes("_hidden") |             isHidden: path.includes("_hidden") | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ interface NoteRow { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface BranchRow { | interface BranchRow { | ||||||
|  |     noteId?: string; | ||||||
|     branchId: string; |     branchId: string; | ||||||
|     componentId: string; |     componentId: string; | ||||||
|     parentNoteId?: string; |     parentNoteId?: string; | ||||||
| @ -157,7 +158,7 @@ export default class LoadResults { | |||||||
|         return Object.keys(this.noteIdToComponentId); |         return Object.keys(this.noteIdToComponentId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isNoteReloaded(noteId: string, componentId = null) { |     isNoteReloaded(noteId: string | undefined, componentId: string | null = null) { | ||||||
|         if (!noteId) { |         if (!noteId) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -124,6 +124,10 @@ function escapeHtml(str: string) { | |||||||
|     return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]); |     return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function escapeQuotes(value: string) { | ||||||
|  |     return value.replaceAll("\"", """); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function formatSize(size: number) { | function formatSize(size: number) { | ||||||
|     size = Math.max(Math.round(size / 1024), 1); |     size = Math.max(Math.round(size / 1024), 1); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
|  * |  * | ||||||
|  * @param noteId of the given note to be fetched. If false, fetches current note. |  * @param noteId of the given note to be fetched. If false, fetches current note. | ||||||
|  */ |  */ | ||||||
| async function fetchNote(noteId = null) { | async function fetchNote(noteId: string | null = null) { | ||||||
|     if (!noteId) { |     if (!noteId) { | ||||||
|         noteId = document.body.getAttribute("data-note-id"); |         noteId = document.body.getAttribute("data-note-id"); | ||||||
|     } |     } | ||||||
| @ -25,3 +25,9 @@ document.addEventListener( | |||||||
|     }, |     }, | ||||||
|     false |     false | ||||||
| ); | ); | ||||||
|  | 
 | ||||||
|  | // workaround to prevent webpack from removing "fetchNote" as dead code:
 | ||||||
|  | // add fetchNote as property to the window object
 | ||||||
|  | Object.defineProperty(window, "fetchNote", { | ||||||
|  |     value: fetchNote | ||||||
|  | }); | ||||||
							
								
								
									
										1
									
								
								src/public/app/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -43,6 +43,7 @@ interface CustomGlobals { | |||||||
|     appCssNoteIds: string[]; |     appCssNoteIds: string[]; | ||||||
|     triliumVersion: string; |     triliumVersion: string; | ||||||
|     TRILIUM_SAFE_MODE: boolean; |     TRILIUM_SAFE_MODE: boolean; | ||||||
|  |     platform?: typeof process.platform; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type RequireMethod = (moduleName: string) => any; | type RequireMethod = (moduleName: string) => any; | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import type AttributeDetailWidget from "./attribute_detail.js"; | |||||||
| import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js"; | import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js"; | ||||||
| import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js"; | import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js"; | ||||||
| import type FNote from "../../entities/fnote.js"; | import type FNote from "../../entities/fnote.js"; | ||||||
|  | import { escapeQuotes } from "../../services/utils.js"; | ||||||
| 
 | 
 | ||||||
| const HELP_TEXT = ` | const HELP_TEXT = ` | ||||||
| <p>${t("attribute_editor.help_text_body1")}</p> | <p>${t("attribute_editor.help_text_body1")}</p> | ||||||
| @ -76,8 +77,8 @@ const TPL = ` | |||||||
| 
 | 
 | ||||||
|     <div class="attribute-list-editor" tabindex="200"></div> |     <div class="attribute-list-editor" tabindex="200"></div> | ||||||
| 
 | 
 | ||||||
|     <div class="bx bx-save save-attributes-button" title="${t("attribute_editor.save_attributes")}"></div> |     <div class="bx bx-save save-attributes-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div> | ||||||
|     <div class="bx bx-plus add-new-attribute-button" title="${t("attribute_editor.add_a_new_attribute")}"></div> |     <div class="bx bx-plus add-new-attribute-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div> | ||||||
| 
 | 
 | ||||||
|     <div class="attribute-errors" style="display: none;"></div> |     <div class="attribute-errors" style="display: none;"></div> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -193,7 +193,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon | |||||||
|      * Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden. |      * Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden. | ||||||
|      * @returns whether the widget is enabled. |      * @returns whether the widget is enabled. | ||||||
|      */ |      */ | ||||||
|     isEnabled() { |     isEnabled(): boolean | null | undefined { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -205,7 +205,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon | |||||||
|      */ |      */ | ||||||
|     doRender() {} |     doRender() {} | ||||||
| 
 | 
 | ||||||
|     toggleInt(show: boolean) { |     toggleInt(show: boolean | null | undefined) { | ||||||
|         this.$widget.toggleClass("hidden-int", !show); |         this.$widget.toggleClass("hidden-int", !show); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,9 +2,11 @@ import options from "../../services/options.js"; | |||||||
| import splitService from "../../services/resizer.js"; | import splitService from "../../services/resizer.js"; | ||||||
| import CommandButtonWidget from "./command_button.js"; | import CommandButtonWidget from "./command_button.js"; | ||||||
| import { t } from "../../services/i18n.js"; | import { t } from "../../services/i18n.js"; | ||||||
|  | import type { EventData } from "../../components/app_context.js"; | ||||||
| 
 | 
 | ||||||
| export default class LeftPaneToggleWidget extends CommandButtonWidget { | export default class LeftPaneToggleWidget extends CommandButtonWidget { | ||||||
|     constructor(isHorizontalLayout) { | 
 | ||||||
|  |     constructor(isHorizontalLayout: boolean) { | ||||||
|         super(); |         super(); | ||||||
| 
 | 
 | ||||||
|         this.class(isHorizontalLayout ? "toggle-button" : "launcher-button"); |         this.class(isHorizontalLayout ? "toggle-button" : "launcher-button"); | ||||||
| @ -32,7 +34,7 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget { | |||||||
|         splitService.setupLeftPaneResizer(options.is("leftPaneVisible")); |         splitService.setupLeftPaneResizer(options.is("leftPaneVisible")); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (loadResults.isOptionReloaded("leftPaneVisible")) { |         if (loadResults.isOptionReloaded("leftPaneVisible")) { | ||||||
|             this.refreshIcon(); |             this.refreshIcon(); | ||||||
|         } |         } | ||||||
| @ -1,6 +1,10 @@ | |||||||
| import NoteContextAwareWidget from "../note_context_aware_widget.js"; | import NoteContextAwareWidget from "../note_context_aware_widget.js"; | ||||||
| import keyboardActionsService from "../../services/keyboard_actions.js"; | import keyboardActionsService from "../../services/keyboard_actions.js"; | ||||||
| import attributeService from "../../services/attributes.js"; | import attributeService from "../../services/attributes.js"; | ||||||
|  | import type CommandButtonWidget from "../buttons/command_button.js"; | ||||||
|  | import type FNote from "../../entities/fnote.js"; | ||||||
|  | import type { NoteType } from "../../entities/fnote.js"; | ||||||
|  | import type { EventData, EventNames } from "../../components/app_context.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="ribbon-container"> | <div class="ribbon-container"> | ||||||
| @ -113,6 +117,16 @@ const TPL = ` | |||||||
| </div>`;
 | </div>`;
 | ||||||
| 
 | 
 | ||||||
| export default class RibbonContainer extends NoteContextAwareWidget { | export default class RibbonContainer extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private lastActiveComponentId?: string | null; | ||||||
|  |     private lastNoteType?: NoteType; | ||||||
|  | 
 | ||||||
|  |     private ribbonWidgets: NoteContextAwareWidget[]; | ||||||
|  |     private buttonWidgets: CommandButtonWidget[]; | ||||||
|  |     private $tabContainer!: JQuery<HTMLElement>; | ||||||
|  |     private $buttonContainer!: JQuery<HTMLElement>; | ||||||
|  |     private $bodyContainer!: JQuery<HTMLElement>; | ||||||
|  | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
| 
 | 
 | ||||||
| @ -122,10 +136,10 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEnabled() { |     isEnabled() { | ||||||
|         return super.isEnabled() && this.noteContext.viewScope.viewMode === "default"; |         return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ribbon(widget) { |     ribbon(widget: NoteContextAwareWidget) { // TODO: Base class
 | ||||||
|         super.child(widget); |         super.child(widget); | ||||||
| 
 | 
 | ||||||
|         this.ribbonWidgets.push(widget); |         this.ribbonWidgets.push(widget); | ||||||
| @ -133,7 +147,7 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     button(widget) { |     button(widget: CommandButtonWidget) { | ||||||
|         super.child(widget); |         super.child(widget); | ||||||
| 
 | 
 | ||||||
|         this.buttonWidgets.push(widget); |         this.buttonWidgets.push(widget); | ||||||
| @ -163,7 +177,7 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     toggleRibbonTab($ribbonTitle, refreshActiveTab = true) { |     toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) { | ||||||
|         const activate = !$ribbonTitle.hasClass("active"); |         const activate = !$ribbonTitle.hasClass("active"); | ||||||
| 
 | 
 | ||||||
|         this.$tabContainer.find(".ribbon-tab-title").removeClass("active"); |         this.$tabContainer.find(".ribbon-tab-title").removeClass("active"); | ||||||
| @ -181,14 +195,15 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|             const activeChild = this.getActiveRibbonWidget(); |             const activeChild = this.getActiveRibbonWidget(); | ||||||
| 
 | 
 | ||||||
|             if (activeChild && (refreshActiveTab || !wasAlreadyActive)) { |             if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) { | ||||||
|                 const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath }); |                 const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath }); | ||||||
| 
 | 
 | ||||||
|                 if (refreshActiveTab) { |                 if (refreshActiveTab) { | ||||||
|                     if (handleEventPromise) { |                     if (handleEventPromise) { | ||||||
|                         handleEventPromise.then(() => activeChild.focus?.()); |                         handleEventPromise.then(() => (activeChild as any).focus()); // TODO: Base class
 | ||||||
|                     } else { |                     } else { | ||||||
|                         activeChild.focus?.(); |                         // TODO: Base class
 | ||||||
|  |                         (activeChild as any)?.focus(); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -203,7 +218,7 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         await super.noteSwitched(); |         await super.noteSwitched(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note, noExplicitActivation = false) { |     async refreshWithNote(note: FNote, noExplicitActivation = false) { | ||||||
|         this.lastNoteType = note.type; |         this.lastNoteType = note.type; | ||||||
| 
 | 
 | ||||||
|         let $ribbonTabToActivate, $lastActiveRibbon; |         let $ribbonTabToActivate, $lastActiveRibbon; | ||||||
| @ -211,7 +226,8 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         this.$tabContainer.empty(); |         this.$tabContainer.empty(); | ||||||
| 
 | 
 | ||||||
|         for (const ribbonWidget of this.ribbonWidgets) { |         for (const ribbonWidget of this.ribbonWidgets) { | ||||||
|             const ret = await ribbonWidget.getTitle(note); |             // TODO: Base class for ribbon widget
 | ||||||
|  |             const ret = await (ribbonWidget as any).getTitle(note); | ||||||
| 
 | 
 | ||||||
|             if (!ret.show) { |             if (!ret.show) { | ||||||
|                 continue; |                 continue; | ||||||
| @ -219,8 +235,8 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|             const $ribbonTitle = $('<div class="ribbon-tab-title">') |             const $ribbonTitle = $('<div class="ribbon-tab-title">') | ||||||
|                 .attr("data-ribbon-component-id", ribbonWidget.componentId) |                 .attr("data-ribbon-component-id", ribbonWidget.componentId) | ||||||
|                 .attr("data-ribbon-component-name", ribbonWidget.name) |                 .attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
 | ||||||
|                 .append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", ribbonWidget.toggleCommand)) |                 .append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", (ribbonWidget as any).toggleCommand)) // TODO: base class
 | ||||||
|                 .append(" ") |                 .append(" ") | ||||||
|                 .append($('<span class="ribbon-tab-title-label">').text(ret.title)); |                 .append($('<span class="ribbon-tab-title-label">').text(ret.title)); | ||||||
| 
 | 
 | ||||||
| @ -238,7 +254,7 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|         keyboardActionsService.getActions().then((actions) => { |         keyboardActionsService.getActions().then((actions) => { | ||||||
|             this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({ |             this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({ | ||||||
|                 title: function () { |                 title: () => { | ||||||
|                     const toggleCommandName = $(this).attr("data-toggle-command"); |                     const toggleCommandName = $(this).attr("data-toggle-command"); | ||||||
|                     const action = actions.find((act) => act.actionName === toggleCommandName); |                     const action = actions.find((act) => act.actionName === toggleCommandName); | ||||||
|                     const title = $(this).attr("data-title"); |                     const title = $(this).attr("data-title"); | ||||||
| @ -246,7 +262,7 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|                     if (action && action.effectiveShortcuts.length > 0) { |                     if (action && action.effectiveShortcuts.length > 0) { | ||||||
|                         return `${title} (${action.effectiveShortcuts.join(", ")})`; |                         return `${title} (${action.effectiveShortcuts.join(", ")})`; | ||||||
|                     } else { |                     } else { | ||||||
|                         return title; |                         return title ?? ""; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
| @ -263,27 +279,27 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isRibbonTabActive(name) { |     isRibbonTabActive(name: string) { | ||||||
|         const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`); |         const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`); | ||||||
| 
 | 
 | ||||||
|         return $ribbonComponent.hasClass("active"); |         return $ribbonComponent.hasClass("active"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ensureOwnedAttributesAreOpen(ntxId) { |     ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) { | ||||||
|         if (this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) { |         if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) { | ||||||
|             this.toggleRibbonTabWithName("ownedAttributes", ntxId); |             this.toggleRibbonTabWithName("ownedAttributes", ntxId); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addNewLabelEvent({ ntxId }) { |     addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) { | ||||||
|         this.ensureOwnedAttributesAreOpen(ntxId); |         this.ensureOwnedAttributesAreOpen(ntxId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addNewRelationEvent({ ntxId }) { |     addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) { | ||||||
|         this.ensureOwnedAttributesAreOpen(ntxId); |         this.ensureOwnedAttributesAreOpen(ntxId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     toggleRibbonTabWithName(name, ntxId) { |     toggleRibbonTabWithName(name: string, ntxId?: string) { | ||||||
|         if (!this.isNoteContext(ntxId)) { |         if (!this.isNoteContext(ntxId)) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| @ -295,23 +311,23 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     handleEvent(name, data) { |     handleEvent<T extends EventNames>(name: T, data: EventData<T>) { | ||||||
|         const PREFIX = "toggleRibbonTab"; |         const PREFIX = "toggleRibbonTab"; | ||||||
| 
 | 
 | ||||||
|         if (name.startsWith(PREFIX)) { |         if (name.startsWith(PREFIX)) { | ||||||
|             let componentName = name.substr(PREFIX.length); |             let componentName = name.substr(PREFIX.length); | ||||||
|             componentName = componentName[0].toLowerCase() + componentName.substr(1); |             componentName = componentName[0].toLowerCase() + componentName.substr(1); | ||||||
| 
 | 
 | ||||||
|             this.toggleRibbonTabWithName(componentName, data.ntxId); |             this.toggleRibbonTabWithName(componentName, (data as any).ntxId); | ||||||
|         } else { |         } else { | ||||||
|             return super.handleEvent(name, data); |             return super.handleEvent(name, data); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async handleEventInChildren(name, data) { |     async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) { | ||||||
|         if (["activeContextChanged", "setNoteContext"].includes(name)) { |         if (["activeContextChanged", "setNoteContext"].includes(name)) { | ||||||
|             // won't trigger .refresh();
 |             // won't trigger .refresh();
 | ||||||
|             await super.handleEventInChildren("setNoteContext", data); |             await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">); | ||||||
|         } else if (this.isEnabled() || name === "initialRenderComplete") { |         } else if (this.isEnabled() || name === "initialRenderComplete") { | ||||||
|             const activeRibbonWidget = this.getActiveRibbonWidget(); |             const activeRibbonWidget = this.getActiveRibbonWidget(); | ||||||
| 
 | 
 | ||||||
| @ -326,8 +342,12 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) { |         if (!this.note) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) { | ||||||
|             // note type influences the list of available ribbon tabs the most
 |             // note type influences the list of available ribbon tabs the most
 | ||||||
|             // check for the type is so that we don't update on each title rename
 |             // check for the type is so that we don't update on each title rename
 | ||||||
|             this.lastNoteType = this.note.type; |             this.lastNoteType = this.note.type; | ||||||
| @ -338,7 +358,7 @@ export default class RibbonContainer extends NoteContextAwareWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     noteTypeMimeChangedEvent() { |     async noteTypeMimeChangedEvent() { | ||||||
|         // We are ignoring the event which triggers a refresh since it is usually already done by a different
 |         // We are ignoring the event which triggers a refresh since it is usually already done by a different
 | ||||||
|         // event and causing a race condition in which the items appear twice.
 |         // event and causing a race condition in which the items appear twice.
 | ||||||
|     } |     } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| import utils from "../../services/utils.js"; | import utils, { escapeQuotes } from "../../services/utils.js"; | ||||||
| import treeService from "../../services/tree.js"; | import treeService from "../../services/tree.js"; | ||||||
| import importService from "../../services/import.js"; | import importService from "../../services/import.js"; | ||||||
| import options from "../../services/options.js"; | import options from "../../services/options.js"; | ||||||
| @ -27,21 +27,21 @@ const TPL = ` | |||||||
|                         <strong>${t("import.options")}:</strong> |                         <strong>${t("import.options")}:</strong> | ||||||
| 
 | 
 | ||||||
|                         <div class="checkbox"> |                         <div class="checkbox"> | ||||||
|                             <label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.safeImportTooltip")}"> |                             <label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.safeImportTooltip"))}"> | ||||||
|                                 <input class="safe-import-checkbox" value="1" type="checkbox" checked> |                                 <input class="safe-import-checkbox" value="1" type="checkbox" checked> | ||||||
|                                 <span>${t("import.safeImport")}</span> |                                 <span>${t("import.safeImport")}</span> | ||||||
|                             </label> |                             </label> | ||||||
|                         </div> |                         </div> | ||||||
| 
 | 
 | ||||||
|                         <div class="checkbox"> |                         <div class="checkbox"> | ||||||
|                             <label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.explodeArchivesTooltip")}"> |                             <label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.explodeArchivesTooltip"))}"> | ||||||
|                                 <input class="explode-archives-checkbox" value="1" type="checkbox" checked> |                                 <input class="explode-archives-checkbox" value="1" type="checkbox" checked> | ||||||
|                                 <span>${t("import.explodeArchives")}</span> |                                 <span>${t("import.explodeArchives")}</span> | ||||||
|                             </label> |                             </label> | ||||||
|                         </div> |                         </div> | ||||||
| 
 | 
 | ||||||
|                         <div class="checkbox"> |                         <div class="checkbox"> | ||||||
|                             <label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.shrinkImagesTooltip")}"> |                             <label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.shrinkImagesTooltip"))}"> | ||||||
|                                 <input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>${t("import.shrinkImages")}</span> |                                 <input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>${t("import.shrinkImages")}</span> | ||||||
|                             </label> |                             </label> | ||||||
|                         </div> |                         </div> | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { t } from "../../services/i18n.js"; | import { t } from "../../services/i18n.js"; | ||||||
| import utils from "../../services/utils.js"; | import utils, { escapeQuotes } from "../../services/utils.js"; | ||||||
| import treeService from "../../services/tree.js"; | import treeService from "../../services/tree.js"; | ||||||
| import importService from "../../services/import.js"; | import importService from "../../services/import.js"; | ||||||
| import options from "../../services/options.js"; | import options from "../../services/options.js"; | ||||||
| @ -24,7 +24,7 @@ const TPL = ` | |||||||
|                     <div class="form-group"> |                     <div class="form-group"> | ||||||
|                         <strong>${t("upload_attachments.options")}:</strong> |                         <strong>${t("upload_attachments.options")}:</strong> | ||||||
|                         <div class="checkbox"> |                         <div class="checkbox"> | ||||||
|                             <label data-bs-toggle="tooltip" title="${t("upload_attachments.tooltip")}"> |                             <label data-bs-toggle="tooltip" title="${escapeQuotes(t("upload_attachments.tooltip"))}"> | ||||||
|                                 <input class="shrink-images-checkbox form-check-input" value="1" type="checkbox" checked> <span>${t("upload_attachments.shrink_images")}</span> |                                 <input class="shrink-images-checkbox form-check-input" value="1" type="checkbox" checked> <span>${t("upload_attachments.shrink_images")}</span> | ||||||
|                             </label> |                             </label> | ||||||
|                         </div> |                         </div> | ||||||
|  | |||||||
| @ -1,6 +1,10 @@ | |||||||
| import attributeService from "../services/attributes.js"; | import attributeService from "../services/attributes.js"; | ||||||
| import NoteContextAwareWidget from "./note_context_aware_widget.js"; | import NoteContextAwareWidget from "./note_context_aware_widget.js"; | ||||||
| import { t } from "../services/i18n.js"; | import { t } from "../services/i18n.js"; | ||||||
|  | import type FNote from "../entities/fnote.js"; | ||||||
|  | import type { EventData } from "../components/app_context.js"; | ||||||
|  | 
 | ||||||
|  | type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="dropdown editability-select-widget"> | <div class="dropdown editability-select-widget"> | ||||||
| @ -9,13 +13,17 @@ const TPL = ` | |||||||
|         width: 300px; |         width: 300px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .editability-dropdown .dropdown-item { | ||||||
|  |         display: block !important; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     .editability-dropdown .dropdown-item div { |     .editability-dropdown .dropdown-item div { | ||||||
|         font-size: small; |         font-size: small; | ||||||
|         color: var(--muted-text-color); |         color: var(--muted-text-color); | ||||||
|         white-space: normal; |         white-space: normal; | ||||||
|     } |     } | ||||||
|     </style> |     </style> | ||||||
|     <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button editability-button"> |     <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle editability-button"> | ||||||
|         <span class="editability-active-desc">${t("editability_select.auto")}</span> |         <span class="editability-active-desc">${t("editability_select.auto")}</span> | ||||||
|         <span class="caret"></span> |         <span class="caret"></span> | ||||||
|     </button> |     </button> | ||||||
| @ -40,9 +48,15 @@ const TPL = ` | |||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
| export default class EditabilitySelectWidget extends NoteContextAwareWidget { | export default class EditabilitySelectWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private dropdown!: bootstrap.Dropdown; | ||||||
|  |     private $editabilityActiveDesc!: JQuery<HTMLElement>; | ||||||
|  | 
 | ||||||
|     doRender() { |     doRender() { | ||||||
|         this.$widget = $(TPL); |         this.$widget = $(TPL); | ||||||
| 
 | 
 | ||||||
|  |         // TODO: Remove once bootstrap is added to webpack.
 | ||||||
|  |         //@ts-ignore
 | ||||||
|         this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); |         this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); | ||||||
| 
 | 
 | ||||||
|         this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc"); |         this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc"); | ||||||
| @ -52,24 +66,28 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|             const editability = $(e.target).closest("[data-editability]").attr("data-editability"); |             const editability = $(e.target).closest("[data-editability]").attr("data-editability"); | ||||||
| 
 | 
 | ||||||
|  |             if (!this.note || !this.noteId) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             for (const ownedAttr of this.note.getOwnedLabels()) { |             for (const ownedAttr of this.note.getOwnedLabels()) { | ||||||
|                 if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) { |                 if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) { | ||||||
|                     await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId); |                     await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (editability !== "auto") { |             if (editability && editability !== "auto") { | ||||||
|                 await attributeService.addLabel(this.noteId, editability); |                 await attributeService.addLabel(this.noteId, editability); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note) { |     async refreshWithNote(note: FNote) { | ||||||
|         let editability = "auto"; |         let editability: Editability = "auto"; | ||||||
| 
 | 
 | ||||||
|         if (this.note.isLabelTruthy("readOnly")) { |         if (this.note?.isLabelTruthy("readOnly")) { | ||||||
|             editability = "readOnly"; |             editability = "readOnly"; | ||||||
|         } else if (this.note.isLabelTruthy("autoReadOnlyDisabled")) { |         } else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) { | ||||||
|             editability = "autoReadOnlyDisabled"; |             editability = "autoReadOnlyDisabled"; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -85,7 +103,7 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget { | |||||||
|         this.$editabilityActiveDesc.text(labels[editability]); |         this.$editabilityActiveDesc.text(labels[editability]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) { |         if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) { | ||||||
|             this.refresh(); |             this.refresh(); | ||||||
|         } |         } | ||||||
| @ -7,6 +7,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; | |||||||
| import linkService from "../../services/link.js"; | import linkService from "../../services/link.js"; | ||||||
| import server from "../../services/server.js"; | import server from "../../services/server.js"; | ||||||
| import froca from "../../services/froca.js"; | import froca from "../../services/froca.js"; | ||||||
|  | import type FNote from "../../entities/fnote.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="backlinks-widget"> | <div class="backlinks-widget"> | ||||||
| @ -64,7 +65,19 @@ const TPL = ` | |||||||
| </div> | </div> | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
|  | // TODO: Deduplicate with server
 | ||||||
|  | interface Backlink { | ||||||
|  |     noteId: string; | ||||||
|  |     relationName?: string; | ||||||
|  |     excerpts?: string[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export default class BacklinksWidget extends NoteContextAwareWidget { | export default class BacklinksWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private $count!: JQuery<HTMLElement>; | ||||||
|  |     private $items!: JQuery<HTMLElement>; | ||||||
|  |     private $ticker!: JQuery<HTMLElement>; | ||||||
|  | 
 | ||||||
|     doRender() { |     doRender() { | ||||||
|         this.$widget = $(TPL); |         this.$widget = $(TPL); | ||||||
|         this.$count = this.$widget.find(".backlinks-count"); |         this.$count = this.$widget.find(".backlinks-count"); | ||||||
| @ -73,7 +86,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|         this.$count.on("click", () => { |         this.$count.on("click", () => { | ||||||
|             this.$items.toggle(); |             this.$items.toggle(); | ||||||
|             this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10); |             this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10); | ||||||
| 
 | 
 | ||||||
|             if (this.$items.is(":visible")) { |             if (this.$items.is(":visible")) { | ||||||
|                 this.renderBacklinks(); |                 this.renderBacklinks(); | ||||||
| @ -83,7 +96,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget { | |||||||
|         this.contentSized(); |         this.contentSized(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note) { |     async refreshWithNote(note: FNote) { | ||||||
|         this.clearItems(); |         this.clearItems(); | ||||||
| 
 | 
 | ||||||
|         if (this.noteContext?.viewScope?.viewMode !== "default") { |         if (this.noteContext?.viewScope?.viewMode !== "default") { | ||||||
| @ -92,7 +105,8 @@ export default class BacklinksWidget extends NoteContextAwareWidget { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // can't use froca since that would count only relations from loaded notes
 |         // can't use froca since that would count only relations from loaded notes
 | ||||||
|         const resp = await server.get(`note-map/${this.noteId}/backlink-count`); |         // TODO: Deduplicate response type
 | ||||||
|  |         const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`); | ||||||
| 
 | 
 | ||||||
|         if (!resp || !resp.count) { |         if (!resp || !resp.count) { | ||||||
|             this.toggle(false); |             this.toggle(false); | ||||||
| @ -106,7 +120,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     toggle(show) { |     toggle(show: boolean) { | ||||||
|         this.$widget.toggleClass("hidden-no-content", !show); |         this.$widget.toggleClass("hidden-no-content", !show); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -121,7 +135,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|         this.$items.empty(); |         this.$items.empty(); | ||||||
| 
 | 
 | ||||||
|         const backlinks = await server.get(`note-map/${this.noteId}/backlinks`); |         const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`); | ||||||
| 
 | 
 | ||||||
|         if (!backlinks.length) { |         if (!backlinks.length) { | ||||||
|             return; |             return; | ||||||
| @ -143,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget { | |||||||
|             if (backlink.relationName) { |             if (backlink.relationName) { | ||||||
|                 $item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`)); |                 $item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`)); | ||||||
|             } else { |             } else { | ||||||
|                 $item.append(...backlink.excerpts); |                 $item.append(...backlink.excerpts ?? []); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             this.$items.append($item); |             this.$items.append($item); | ||||||
| @ -40,7 +40,7 @@ export default class GeoMapWidget extends NoteContextAwareWidget { | |||||||
|                 const L = (await import("leaflet")).default; |                 const L = (await import("leaflet")).default; | ||||||
| 
 | 
 | ||||||
|                 const map = L.map(this.$container[0], { |                 const map = L.map(this.$container[0], { | ||||||
| 
 |                     worldCopyJump: true | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|                 this.map = map; |                 this.map = map; | ||||||
|  | |||||||
| @ -10,9 +10,9 @@ import type NoteContext from "../components/note_context.js"; | |||||||
| class NoteContextAwareWidget extends BasicWidget { | class NoteContextAwareWidget extends BasicWidget { | ||||||
|     protected noteContext?: NoteContext; |     protected noteContext?: NoteContext; | ||||||
| 
 | 
 | ||||||
|     isNoteContext(ntxId: string | null | undefined) { |     isNoteContext(ntxId: string | string[] | null | undefined) { | ||||||
|         if (Array.isArray(ntxId)) { |         if (Array.isArray(ntxId)) { | ||||||
|             return this.noteContext && ntxId.includes(this.noteContext.ntxId); |             return this.noteContext && this.noteContext.ntxId && ntxId.includes(this.noteContext.ntxId); | ||||||
|         } else { |         } else { | ||||||
|             return this.noteContext && this.noteContext.ntxId === ntxId; |             return this.noteContext && this.noteContext.ntxId === ntxId; | ||||||
|         } |         } | ||||||
| @ -54,7 +54,7 @@ class NoteContextAwareWidget extends BasicWidget { | |||||||
|      * |      * | ||||||
|      * @returns true when an active note exists |      * @returns true when an active note exists | ||||||
|      */ |      */ | ||||||
|     isEnabled() { |     isEnabled(): boolean | null | undefined { | ||||||
|         return !!this.note; |         return !!this.note; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -147,11 +147,14 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | |||||||
|      */ |      */ | ||||||
|     checkFullHeight() { |     checkFullHeight() { | ||||||
|         // https://github.com/zadam/trilium/issues/2522
 |         // https://github.com/zadam/trilium/issues/2522
 | ||||||
|         this.$widget.toggleClass( |         const isBackendNote = this.noteContext?.noteId === "_backendLog"; | ||||||
|             "full-height", |         const isSqlNote = this.mime === "text/x-sqlite;schema=trilium"; | ||||||
|             (!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") || |         const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type); | ||||||
|                 this.noteContext.viewScope.viewMode === "attachments" |         const isFullHeight = (!this.noteContext.hasNoteList() && isFullHeightNoteType && !isSqlNote) | ||||||
|         ); |             || this.noteContext.viewScope.viewMode === "attachments" | ||||||
|  |             || isBackendNote; | ||||||
|  | 
 | ||||||
|  |         this.$widget.toggleClass("full-height", isFullHeight); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     getTypeWidget() { |     getTypeWidget() { | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| import NoteContextAwareWidget from "./note_context_aware_widget.js"; | import NoteContextAwareWidget from "./note_context_aware_widget.js"; | ||||||
| import NoteListRenderer from "../services/note_list_renderer.js"; | import NoteListRenderer from "../services/note_list_renderer.js"; | ||||||
|  | import type FNote from "../entities/fnote.js"; | ||||||
|  | import type { EventData } from "../components/app_context.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-list-widget"> | <div class="note-list-widget"> | ||||||
| @ -19,8 +21,14 @@ const TPL = ` | |||||||
| </div>`;
 | </div>`;
 | ||||||
| 
 | 
 | ||||||
| export default class NoteListWidget extends NoteContextAwareWidget { | export default class NoteListWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private $content!: JQuery<HTMLElement>; | ||||||
|  |     private isIntersecting?: boolean; | ||||||
|  |     private noteIdRefreshed?: string; | ||||||
|  |     private shownNoteId?: string | null; | ||||||
|  | 
 | ||||||
|     isEnabled() { |     isEnabled() { | ||||||
|         return super.isEnabled() && this.noteContext.hasNoteList(); |         return super.isEnabled() && this.noteContext?.hasNoteList(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     doRender() { |     doRender() { | ||||||
| @ -50,13 +58,13 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|         // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
 |         // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
 | ||||||
|         // console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId);
 |         // console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId);
 | ||||||
| 
 | 
 | ||||||
|         if (this.isIntersecting && this.noteIdRefreshed === this.noteId && this.shownNoteId !== this.noteId) { |         if (this.note && this.isIntersecting && this.noteIdRefreshed === this.noteId && this.shownNoteId !== this.noteId) { | ||||||
|             this.shownNoteId = this.noteId; |             this.shownNoteId = this.noteId; | ||||||
|             this.renderNoteList(this.note); |             this.renderNoteList(this.note); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async renderNoteList(note) { |     async renderNoteList(note: FNote) { | ||||||
|         const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds()); |         const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds()); | ||||||
|         await noteListRenderer.renderList(); |         await noteListRenderer.renderList(); | ||||||
|     } |     } | ||||||
| @ -67,8 +75,8 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|         await super.refresh(); |         await super.refresh(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshNoteListEvent({ noteId }) { |     async refreshNoteListEvent({ noteId }: EventData<"refreshNoteList">) { | ||||||
|         if (this.isNote(noteId)) { |         if (this.isNote(noteId) && this.note) { | ||||||
|             await this.renderNoteList(this.note); |             await this.renderNoteList(this.note); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -78,7 +86,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|      * If it's evaluated before note detail, then it's clearly intersected (visible) although after note detail load |      * If it's evaluated before note detail, then it's clearly intersected (visible) although after note detail load | ||||||
|      * it is not intersected (visible) anymore. |      * it is not intersected (visible) anymore. | ||||||
|      */ |      */ | ||||||
|     noteDetailRefreshedEvent({ ntxId }) { |     noteDetailRefreshedEvent({ ntxId }: EventData<"noteDetailRefreshed">) { | ||||||
|         if (!this.isNoteContext(ntxId)) { |         if (!this.isNoteContext(ntxId)) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @ -88,14 +96,14 @@ export default class NoteListWidget extends NoteContextAwareWidget { | |||||||
|         setTimeout(() => this.checkRenderStatus(), 100); |         setTimeout(() => this.checkRenderStatus(), 100); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     notesReloadedEvent({ noteIds }) { |     notesReloadedEvent({ noteIds }: EventData<"notesReloaded">) { | ||||||
|         if (noteIds.includes(this.noteId)) { |         if (this.noteId && noteIds.includes(this.noteId)) { | ||||||
|             this.refresh(); |             this.refresh(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && ["viewType", "expanded", "pageSize"].includes(attr.name))) { |         if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) { | ||||||
|             this.shownNoteId = null; // force render
 |             this.shownNoteId = null; // force render
 | ||||||
| 
 | 
 | ||||||
|             this.checkRenderStatus(); |             this.checkRenderStatus(); | ||||||
| @ -163,7 +163,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { | |||||||
|     private themeStyle!: string; |     private themeStyle!: string; | ||||||
|     private $container!: JQuery<HTMLElement>; |     private $container!: JQuery<HTMLElement>; | ||||||
|     private $styleResolver!: JQuery<HTMLElement>; |     private $styleResolver!: JQuery<HTMLElement>; | ||||||
|     private graph!: ForceGraph; |     graph!: ForceGraph; | ||||||
|     private noteIdToSizeMap!: Record<string, number>; |     private noteIdToSizeMap!: Record<string, number>; | ||||||
|     private zoomLevel!: number; |     private zoomLevel!: number; | ||||||
|     private nodes!: Node[]; |     private nodes!: Node[]; | ||||||
|  | |||||||
| @ -3,10 +3,11 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; | |||||||
| import protectedSessionHolder from "../services/protected_session_holder.js"; | import protectedSessionHolder from "../services/protected_session_holder.js"; | ||||||
| import server from "../services/server.js"; | import server from "../services/server.js"; | ||||||
| import SpacedUpdate from "../services/spaced_update.js"; | import SpacedUpdate from "../services/spaced_update.js"; | ||||||
| import appContext from "../components/app_context.js"; | import appContext, { type EventData } from "../components/app_context.js"; | ||||||
| import branchService from "../services/branches.js"; | import branchService from "../services/branches.js"; | ||||||
| import shortcutService from "../services/shortcuts.js"; | import shortcutService from "../services/shortcuts.js"; | ||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
|  | import type FNote from "../entities/fnote.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-title-widget"> | <div class="note-title-widget"> | ||||||
| @ -33,13 +34,20 @@ const TPL = ` | |||||||
| </div>`;
 | </div>`;
 | ||||||
| 
 | 
 | ||||||
| export default class NoteTitleWidget extends NoteContextAwareWidget { | export default class NoteTitleWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private $noteTitle!: JQuery<HTMLElement>; | ||||||
|  |     private deleteNoteOnEscape: boolean; | ||||||
|  |     private spacedUpdate: SpacedUpdate; | ||||||
|  | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
| 
 | 
 | ||||||
|         this.spacedUpdate = new SpacedUpdate(async () => { |         this.spacedUpdate = new SpacedUpdate(async () => { | ||||||
|             const title = this.$noteTitle.val(); |             const title = this.$noteTitle.val(); | ||||||
| 
 | 
 | ||||||
|  |             if (this.note) { | ||||||
|                 protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); |                 protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             await server.put(`notes/${this.noteId}/title`, { title }, this.componentId); |             await server.put(`notes/${this.noteId}/title`, { title }, this.componentId); | ||||||
|         }); |         }); | ||||||
| @ -62,37 +70,36 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { | |||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         shortcutService.bindElShortcut(this.$noteTitle, "esc", () => { |         shortcutService.bindElShortcut(this.$noteTitle, "esc", () => { | ||||||
|             if (this.deleteNoteOnEscape && this.noteContext.isActive()) { |             if (this.deleteNoteOnEscape && this.noteContext?.isActive() && this.noteContext?.note) { | ||||||
|                 branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch)); |                 branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch)); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         shortcutService.bindElShortcut(this.$noteTitle, "return", () => { |         shortcutService.bindElShortcut(this.$noteTitle, "return", () => { | ||||||
|             this.triggerCommand("focusOnDetail", { ntxId: this.noteContext.ntxId }); |             this.triggerCommand("focusOnDetail", { ntxId: this.noteContext?.ntxId }); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note) { |     async refreshWithNote(note: FNote) { | ||||||
|         const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext.viewScope.viewMode !== "default"; |         const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext?.viewScope?.viewMode !== "default"; | ||||||
| 
 | 
 | ||||||
|         this.$noteTitle.val(isReadOnly ? await this.noteContext.getNavigationTitle() : note.title); |         this.$noteTitle.val(isReadOnly ? await this.noteContext?.getNavigationTitle() || "" : note.title); | ||||||
|         this.$noteTitle.prop("readonly", isReadOnly); |         this.$noteTitle.prop("readonly", isReadOnly); | ||||||
| 
 | 
 | ||||||
|         this.setProtectedStatus(note); |         this.setProtectedStatus(note); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @param {FNote} note */ |     setProtectedStatus(note: FNote) { | ||||||
|     setProtectedStatus(note) { |  | ||||||
|         this.$noteTitle.toggleClass("protected", !!note.isProtected); |         this.$noteTitle.toggleClass("protected", !!note.isProtected); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async beforeNoteSwitchEvent({ noteContext }) { |     async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) { | ||||||
|         if (this.isNoteContext(noteContext.ntxId)) { |         if (this.isNoteContext(noteContext.ntxId)) { | ||||||
|             await this.spacedUpdate.updateNowIfNecessary(); |             await this.spacedUpdate.updateNowIfNecessary(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async beforeNoteContextRemoveEvent({ ntxIds }) { |     async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) { | ||||||
|         if (this.isNoteContext(ntxIds)) { |         if (this.isNoteContext(ntxIds)) { | ||||||
|             await this.spacedUpdate.updateNowIfNecessary(); |             await this.spacedUpdate.updateNowIfNecessary(); | ||||||
|         } |         } | ||||||
| @ -112,8 +119,8 @@ export default class NoteTitleWidget extends NoteContextAwareWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (loadResults.isNoteReloaded(this.noteId)) { |         if (loadResults.isNoteReloaded(this.noteId) && this.note) { | ||||||
|             // not updating the title specifically since the synced title might be older than what the user is currently typing
 |             // not updating the title specifically since the synced title might be older than what the user is currently typing
 | ||||||
|             this.setProtectedStatus(this.note); |             this.setProtectedStatus(this.note); | ||||||
|         } |         } | ||||||
| @ -21,6 +21,7 @@ const NOTE_TYPES = [ | |||||||
|     { type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true }, |     { type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true }, | ||||||
|     { type: "book", mime: "", title: t("note_types.book"), selectable: true }, |     { type: "book", mime: "", title: t("note_types.book"), selectable: true }, | ||||||
|     { type: "webView", mime: "", title: t("note_types.web-view"), selectable: true }, |     { type: "webView", mime: "", title: t("note_types.web-view"), selectable: true }, | ||||||
|  |     { type: "geoMap", mime: "application/json", title: t("note_types.geo-map"), selectable: true }, | ||||||
|     { type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true } |     { type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true } | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ import { t } from "../../services/i18n.js"; | |||||||
| import NoteContextAwareWidget from "../note_context_aware_widget.js"; | import NoteContextAwareWidget from "../note_context_aware_widget.js"; | ||||||
| import server from "../../services/server.js"; | import server from "../../services/server.js"; | ||||||
| import utils from "../../services/utils.js"; | import utils from "../../services/utils.js"; | ||||||
|  | import type { EventData } from "../../components/app_context.js"; | ||||||
|  | import type FNote from "../../entities/fnote.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-info-widget"> | <div class="note-info-widget"> | ||||||
| @ -61,7 +63,33 @@ const TPL = ` | |||||||
| </div> | </div> | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
|  | // TODO: Deduplicate with server
 | ||||||
|  | interface NoteSizeResponse { | ||||||
|  |     noteSize: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface SubtreeSizeResponse { | ||||||
|  |     subTreeNoteCount: number; | ||||||
|  |     subTreeSize: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface MetadataResponse { | ||||||
|  |     dateCreated: number; | ||||||
|  |     dateModified: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export default class NoteInfoWidget extends NoteContextAwareWidget { | export default class NoteInfoWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private $noteId!: JQuery<HTMLElement>; | ||||||
|  |     private $dateCreated!: JQuery<HTMLElement>; | ||||||
|  |     private $dateModified!: JQuery<HTMLElement>; | ||||||
|  |     private $type!: JQuery<HTMLElement>; | ||||||
|  |     private $mime!: JQuery<HTMLElement>; | ||||||
|  |     private $noteSizesWrapper!: JQuery<HTMLElement>; | ||||||
|  |     private $noteSize!: JQuery<HTMLElement>; | ||||||
|  |     private $subTreeSize!: JQuery<HTMLElement>; | ||||||
|  |     private $calculateButton!: JQuery<HTMLElement>; | ||||||
|  | 
 | ||||||
|     get name() { |     get name() { | ||||||
|         return "noteInfo"; |         return "noteInfo"; | ||||||
|     } |     } | ||||||
| @ -71,7 +99,7 @@ export default class NoteInfoWidget extends NoteContextAwareWidget { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEnabled() { |     isEnabled() { | ||||||
|         return this.note; |         return !!this.note; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     getTitle() { |     getTitle() { | ||||||
| @ -104,10 +132,10 @@ export default class NoteInfoWidget extends NoteContextAwareWidget { | |||||||
|             this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>')); |             this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>')); | ||||||
|             this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>')); |             this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>')); | ||||||
| 
 | 
 | ||||||
|             const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`); |             const noteSizeResp = await server.get<NoteSizeResponse>(`stats/note-size/${this.noteId}`); | ||||||
|             this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize)); |             this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize)); | ||||||
| 
 | 
 | ||||||
|             const subTreeResp = await server.get(`stats/subtree-size/${this.noteId}`); |             const subTreeResp = await server.get<SubtreeSizeResponse>(`stats/subtree-size/${this.noteId}`); | ||||||
| 
 | 
 | ||||||
|             if (subTreeResp.subTreeNoteCount > 1) { |             if (subTreeResp.subTreeNoteCount > 1) { | ||||||
|                 this.$subTreeSize.text(t("note_info_widget.subtree_size", { size: utils.formatSize(subTreeResp.subTreeSize), count: subTreeResp.subTreeNoteCount })); |                 this.$subTreeSize.text(t("note_info_widget.subtree_size", { size: utils.formatSize(subTreeResp.subTreeSize), count: subTreeResp.subTreeNoteCount })); | ||||||
| @ -117,8 +145,8 @@ export default class NoteInfoWidget extends NoteContextAwareWidget { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note) { |     async refreshWithNote(note: FNote) { | ||||||
|         const metadata = await server.get(`notes/${this.noteId}/metadata`); |         const metadata = await server.get<MetadataResponse>(`notes/${this.noteId}/metadata`); | ||||||
| 
 | 
 | ||||||
|         this.$noteId.text(note.noteId); |         this.$noteId.text(note.noteId); | ||||||
|         this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated); |         this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated); | ||||||
| @ -137,8 +165,8 @@ export default class NoteInfoWidget extends NoteContextAwareWidget { | |||||||
|         this.$noteSizesWrapper.hide(); |         this.$noteSizesWrapper.hide(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId)) { |         if (this.noteId && (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId))) { | ||||||
|             this.refresh(); |             this.refresh(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -33,6 +33,13 @@ const TPL = ` | |||||||
| </div>`;
 | </div>`;
 | ||||||
| 
 | 
 | ||||||
| export default class NoteMapRibbonWidget extends NoteContextAwareWidget { | export default class NoteMapRibbonWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private openState!: "small" | "full"; | ||||||
|  |     private noteMapWidget: NoteMapWidget; | ||||||
|  |     private $container!: JQuery<HTMLElement>; | ||||||
|  |     private $openFullButton!: JQuery<HTMLElement>; | ||||||
|  |     private $collapseButton!: JQuery<HTMLElement>; | ||||||
|  | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
| 
 | 
 | ||||||
| @ -106,7 +113,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|     setSmallSize() { |     setSmallSize() { | ||||||
|         const SMALL_SIZE_HEIGHT = 300; |         const SMALL_SIZE_HEIGHT = 300; | ||||||
|         const width = this.$widget.width(); |         const width = this.$widget.width() ?? 0; | ||||||
| 
 | 
 | ||||||
|         this.$widget.find(".note-map-container").height(SMALL_SIZE_HEIGHT).width(width); |         this.$widget.find(".note-map-container").height(SMALL_SIZE_HEIGHT).width(width); | ||||||
|     } |     } | ||||||
| @ -114,9 +121,11 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget { | |||||||
|     setFullHeight() { |     setFullHeight() { | ||||||
|         const { top } = this.$widget[0].getBoundingClientRect(); |         const { top } = this.$widget[0].getBoundingClientRect(); | ||||||
| 
 | 
 | ||||||
|         const height = $(window).height() - top; |         const height = ($(window).height() ?? 0) - top; | ||||||
|         const width = this.$widget.width(); |         const width = (this.$widget.width() ?? 0); | ||||||
| 
 | 
 | ||||||
|         this.$widget.find(".note-map-container").height(height).width(width); |         this.$widget.find(".note-map-container") | ||||||
|  |             .height(height) | ||||||
|  |             .width(width); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -2,6 +2,9 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; | |||||||
| import treeService from "../../services/tree.js"; | import treeService from "../../services/tree.js"; | ||||||
| import linkService from "../../services/link.js"; | import linkService from "../../services/link.js"; | ||||||
| import { t } from "../../services/i18n.js"; | import { t } from "../../services/i18n.js"; | ||||||
|  | import type FNote from "../../entities/fnote.js"; | ||||||
|  | import type { NotePathRecord } from "../../entities/fnote.js"; | ||||||
|  | import type { EventData } from "../../components/app_context.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-paths-widget"> | <div class="note-paths-widget"> | ||||||
| @ -37,6 +40,10 @@ const TPL = ` | |||||||
| </div>`;
 | </div>`;
 | ||||||
| 
 | 
 | ||||||
| export default class NotePathsWidget extends NoteContextAwareWidget { | export default class NotePathsWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private $notePathIntro!: JQuery<HTMLElement>; | ||||||
|  |     private $notePathList!: JQuery<HTMLElement>; | ||||||
|  | 
 | ||||||
|     get name() { |     get name() { | ||||||
|         return "notePaths"; |         return "notePaths"; | ||||||
|     } |     } | ||||||
| @ -59,13 +66,12 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|         this.$notePathIntro = this.$widget.find(".note-path-intro"); |         this.$notePathIntro = this.$widget.find(".note-path-intro"); | ||||||
|         this.$notePathList = this.$widget.find(".note-path-list"); |         this.$notePathList = this.$widget.find(".note-path-list"); | ||||||
|         this.$widget.on("show.bs.dropdown", () => this.renderDropdown()); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note) { |     async refreshWithNote(note: FNote) { | ||||||
|         this.$notePathList.empty(); |         this.$notePathList.empty(); | ||||||
| 
 | 
 | ||||||
|         if (this.noteId === "root") { |         if (!this.note || this.noteId === "root") { | ||||||
|             this.$notePathList.empty().append(await this.getRenderedPath("root")); |             this.$notePathList.empty().append(await this.getRenderedPath("root")); | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
| @ -90,7 +96,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||||||
|         this.$notePathList.empty().append(...renderedPaths); |         this.$notePathList.empty().append(...renderedPaths); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getRenderedPath(notePath, notePathRecord = null) { |     async getRenderedPath(notePath: string, notePathRecord: NotePathRecord | null = null) { | ||||||
|         const title = await treeService.getNotePathTitle(notePath); |         const title = await treeService.getNotePathTitle(notePath); | ||||||
| 
 | 
 | ||||||
|         const $noteLink = await linkService.createLink(notePath, { title }); |         const $noteLink = await linkService.createLink(notePath, { title }); | ||||||
| @ -128,8 +134,9 @@ export default class NotePathsWidget extends NoteContextAwareWidget { | |||||||
|         return $("<li>").append($noteLink); |         return $("<li>").append($noteLink); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || loadResults.isNoteReloaded(this.noteId)) { |         if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || | ||||||
|  |             (this.noteId != null && loadResults.isNoteReloaded(this.noteId))) { | ||||||
|             this.refresh(); |             this.refresh(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -3,6 +3,8 @@ import linkService from "../../services/link.js"; | |||||||
| import server from "../../services/server.js"; | import server from "../../services/server.js"; | ||||||
| import froca from "../../services/froca.js"; | import froca from "../../services/froca.js"; | ||||||
| import NoteContextAwareWidget from "../note_context_aware_widget.js"; | import NoteContextAwareWidget from "../note_context_aware_widget.js"; | ||||||
|  | import type FNote from "../../entities/fnote.js"; | ||||||
|  | import type { EventData } from "../../components/app_context.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="similar-notes-widget"> | <div class="similar-notes-widget"> | ||||||
| @ -31,7 +33,20 @@ const TPL = ` | |||||||
| </div> | </div> | ||||||
| `;
 | `;
 | ||||||
| 
 | 
 | ||||||
|  | // TODO: Deduplicate with server
 | ||||||
|  | interface SimilarNote { | ||||||
|  |     score: number; | ||||||
|  |     notePath: string[]; | ||||||
|  |     noteId: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| export default class SimilarNotesWidget extends NoteContextAwareWidget { | export default class SimilarNotesWidget extends NoteContextAwareWidget { | ||||||
|  | 
 | ||||||
|  |     private $similarNotesWrapper!: JQuery<HTMLElement>; | ||||||
|  |     private title?: string; | ||||||
|  |     private rendered?: boolean; | ||||||
|  | 
 | ||||||
|     get name() { |     get name() { | ||||||
|         return "similarNotes"; |         return "similarNotes"; | ||||||
|     } |     } | ||||||
| @ -41,7 +56,7 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isEnabled() { |     isEnabled() { | ||||||
|         return super.isEnabled() && this.note.type !== "search" && !this.note.isLabelTruthy("similarNotesWidgetDisabled"); |         return super.isEnabled() && this.note?.type !== "search" && !this.note?.isLabelTruthy("similarNotesWidgetDisabled"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     getTitle() { |     getTitle() { | ||||||
| @ -59,11 +74,15 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget { | |||||||
|         this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper"); |         this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async refreshWithNote(note) { |     async refreshWithNote(note: FNote) { | ||||||
|  |         if (!this.note) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // remember which title was when we found the similar notes
 |         // remember which title was when we found the similar notes
 | ||||||
|         this.title = this.note.title; |         this.title = this.note.title; | ||||||
| 
 | 
 | ||||||
|         const similarNotes = await server.get(`similar-notes/${this.noteId}`); |         const similarNotes = await server.get<SimilarNote[]>(`similar-notes/${this.noteId}`); | ||||||
| 
 | 
 | ||||||
|         if (similarNotes.length === 0) { |         if (similarNotes.length === 0) { | ||||||
|             this.$similarNotesWrapper.empty().append(t("similar_notes.no_similar_notes_found")); |             this.$similarNotesWrapper.empty().append(t("similar_notes.no_similar_notes_found")); | ||||||
| @ -92,7 +111,7 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget { | |||||||
|         this.$similarNotesWrapper.empty().append($list); |         this.$similarNotesWrapper.empty().append($list); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     entitiesReloadedEvent({ loadResults }) { |     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||||
|         if (this.note && this.title !== this.note.title) { |         if (this.note && this.title !== this.note.title) { | ||||||
|             this.rendered = false; |             this.rendered = false; | ||||||
| 
 | 
 | ||||||
| @ -3,6 +3,7 @@ import BasicWidget from "./basic_widget.js"; | |||||||
| import ws from "../services/ws.js"; | import ws from "../services/ws.js"; | ||||||
| import options from "../services/options.js"; | import options from "../services/options.js"; | ||||||
| import syncService from "../services/sync.js"; | import syncService from "../services/sync.js"; | ||||||
|  | import { escapeQuotes } from "../services/utils.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="sync-status-widget launcher-button"> | <div class="sync-status-widget launcher-button"> | ||||||
| @ -41,29 +42,29 @@ const TPL = ` | |||||||
|     <div class="sync-status"> |     <div class="sync-status"> | ||||||
|         <span class="sync-status-icon sync-status-unknown bx bx-time" |         <span class="sync-status-icon sync-status-unknown bx bx-time" | ||||||
|               data-bs-toggle="tooltip" |               data-bs-toggle="tooltip" | ||||||
|               title="${t("sync_status.unknown")}"> |               title="${escapeQuotes(t("sync_status.unknown"))}"> | ||||||
|         </span> |         </span> | ||||||
|         <span class="sync-status-icon sync-status-connected-with-changes bx bx-wifi" |         <span class="sync-status-icon sync-status-connected-with-changes bx bx-wifi" | ||||||
|               data-bs-toggle="tooltip" |               data-bs-toggle="tooltip" | ||||||
|               title="${t("sync_status.connected_with_changes")}"> |               title="${escapeQuotes(t("sync_status.connected_with_changes"))}"> | ||||||
|             <span class="bx bxs-star sync-status-sub-icon"></span> |             <span class="bx bxs-star sync-status-sub-icon"></span> | ||||||
|         </span> |         </span> | ||||||
|         <span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi" |         <span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi" | ||||||
|               data-bs-toggle="tooltip" |               data-bs-toggle="tooltip" | ||||||
|               title="${t("sync_status.connected_no_changes")}"> |               title="${escapeQuotes(t("sync_status.connected_no_changes"))}"> | ||||||
|         </span> |         </span> | ||||||
|         <span class="sync-status-icon sync-status-disconnected-with-changes bx bx-wifi-off" |         <span class="sync-status-icon sync-status-disconnected-with-changes bx bx-wifi-off" | ||||||
|               data-bs-toggle="tooltip" |               data-bs-toggle="tooltip" | ||||||
|               title="${t("sync_status.disconnected_with_changes")}"> |               title="${escapeQuotes(t("sync_status.disconnected_with_changes"))}"> | ||||||
|             <span class="bx bxs-star sync-status-sub-icon"></span> |             <span class="bx bxs-star sync-status-sub-icon"></span> | ||||||
|         </span> |         </span> | ||||||
|         <span class="sync-status-icon sync-status-disconnected-no-changes bx bx-wifi-off" |         <span class="sync-status-icon sync-status-disconnected-no-changes bx bx-wifi-off" | ||||||
|               data-bs-toggle="tooltip" |               data-bs-toggle="tooltip" | ||||||
|               title="${t("sync_status.disconnected_no_changes")}"> |               title="${escapeQuotes(t("sync_status.disconnected_no_changes"))}"> | ||||||
|         </span> |         </span> | ||||||
|         <span class="sync-status-icon sync-status-in-progress bx bx-analyse bx-spin" |         <span class="sync-status-icon sync-status-in-progress bx bx-analyse bx-spin" | ||||||
|               data-bs-toggle="tooltip" |               data-bs-toggle="tooltip" | ||||||
|               title="${t("sync_status.in_progress")}"> |               title="${escapeQuotes(t("sync_status.in_progress"))}"> | ||||||
|         </span> |         </span> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 10 KiB | 
| @ -26,6 +26,11 @@ | |||||||
|         border-radius: 2pt !important; |         border-radius: 2pt !important; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     span[style] { | ||||||
|  |         print-color-adjust: exact; | ||||||
|  |         -webkit-print-color-adjust: exact; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /* Fix visibility of checkbox checkmarks |     /* Fix visibility of checkbox checkmarks | ||||||
|        see https://github.com/TriliumNext/Notes/issues/901 */ |        see https://github.com/TriliumNext/Notes/issues/901 */ | ||||||
|     .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 { | ||||||
|  | |||||||
| @ -396,6 +396,10 @@ body.desktop .dropdown-menu { | |||||||
|     color: var(--dropdown-item-icon-destructive-color); |     color: var(--dropdown-item-icon-destructive-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .dropdown-item > span:not([class]) { | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .CodeMirror { | .CodeMirror { | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     background: inherit; |     background: inherit; | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child { | |||||||
|     padding: 0px 10px; |     padding: 0px 10px; | ||||||
|     letter-spacing: 0.5px; |     letter-spacing: 0.5px; | ||||||
|     font-weight: bold; |     font-weight: bold; | ||||||
|  |     top: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .attachment-content-wrapper pre code, | .attachment-content-wrapper pre code, | ||||||
|  | |||||||
| @ -1339,7 +1339,7 @@ body .calendar-dropdown-widget .calendar-body a:hover { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* Item title for deleted notes */ | /* Item title for deleted notes */ | ||||||
| .recent-changes-content ul li.deleted-note .note-title { | .recent-changes-content ul li.deleted-note .note-title > .note-title { | ||||||
|     text-decoration: line-through; |     text-decoration: line-through; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1350,7 +1350,7 @@ | |||||||
|     "mermaid-diagram": "Mermaid Diagram", |     "mermaid-diagram": "Mermaid Diagram", | ||||||
|     "canvas": "Canvas", |     "canvas": "Canvas", | ||||||
|     "web-view": "Webansicht", |     "web-view": "Webansicht", | ||||||
|     "mind-map": "Mind Map (Beta)", |     "mind-map": "Mind Map", | ||||||
|     "file": "Datei", |     "file": "Datei", | ||||||
|     "image": "Bild", |     "image": "Bild", | ||||||
|     "launcher": "Launcher", |     "launcher": "Launcher", | ||||||
|  | |||||||
| @ -1403,7 +1403,7 @@ | |||||||
|     "mermaid-diagram": "Mermaid Diagram", |     "mermaid-diagram": "Mermaid Diagram", | ||||||
|     "canvas": "Canvas", |     "canvas": "Canvas", | ||||||
|     "web-view": "Web View", |     "web-view": "Web View", | ||||||
|     "mind-map": "Mind Map (Beta)", |     "mind-map": "Mind Map", | ||||||
|     "file": "File", |     "file": "File", | ||||||
|     "image": "Image", |     "image": "Image", | ||||||
|     "launcher": "Launcher", |     "launcher": "Launcher", | ||||||
|  | |||||||
| @ -1403,7 +1403,7 @@ | |||||||
|     "mermaid-diagram": "Diagrama Mermaid", |     "mermaid-diagram": "Diagrama Mermaid", | ||||||
|     "canvas": "Lienzo", |     "canvas": "Lienzo", | ||||||
|     "web-view": "Vista Web", |     "web-view": "Vista Web", | ||||||
|     "mind-map": "Mapa Mental (beta)", |     "mind-map": "Mapa Mental", | ||||||
|     "file": "Archivo", |     "file": "Archivo", | ||||||
|     "image": "Imagen", |     "image": "Imagen", | ||||||
|     "launcher": "Lanzador", |     "launcher": "Lanzador", | ||||||
|  | |||||||
| @ -1351,7 +1351,7 @@ | |||||||
|     "mermaid-diagram": "Diagramme Mermaid", |     "mermaid-diagram": "Diagramme Mermaid", | ||||||
|     "canvas": "Canevas", |     "canvas": "Canevas", | ||||||
|     "web-view": "Affichage Web", |     "web-view": "Affichage Web", | ||||||
|     "mind-map": "Carte mentale (Beta)", |     "mind-map": "Carte mentale", | ||||||
|     "file": "Fichier", |     "file": "Fichier", | ||||||
|     "image": "Image", |     "image": "Image", | ||||||
|     "launcher": "Raccourci", |     "launcher": "Raccourci", | ||||||
|  | |||||||
| @ -1367,7 +1367,7 @@ | |||||||
|     "canvas": "Schiță", |     "canvas": "Schiță", | ||||||
|     "code": "Cod sursă", |     "code": "Cod sursă", | ||||||
|     "mermaid-diagram": "Diagramă Mermaid", |     "mermaid-diagram": "Diagramă Mermaid", | ||||||
|     "mind-map": "Hartă mentală (beta)", |     "mind-map": "Hartă mentală", | ||||||
|     "note-map": "Hartă notițe", |     "note-map": "Hartă notițe", | ||||||
|     "relation-map": "Hartă relații", |     "relation-map": "Hartă relații", | ||||||
|     "render-note": "Randare notiță", |     "render-note": "Randare notiță", | ||||||
|  | |||||||
| @ -6,6 +6,12 @@ import type BNote from "../../becca/entities/bnote.js"; | |||||||
| import type BAttribute from "../../becca/entities/battribute.js"; | import type BAttribute from "../../becca/entities/battribute.js"; | ||||||
| import type { Request } from "express"; | import type { Request } from "express"; | ||||||
| 
 | 
 | ||||||
|  | interface Backlink { | ||||||
|  |     noteId: string; | ||||||
|  |     relationName?: string; | ||||||
|  |     excerpts?: string[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function buildDescendantCountMap(noteIdsToCount: string[]) { | function buildDescendantCountMap(noteIdsToCount: string[]) { | ||||||
|     if (!Array.isArray(noteIdsToCount)) { |     if (!Array.isArray(noteIdsToCount)) { | ||||||
|         throw new Error("noteIdsToCount: type error"); |         throw new Error("noteIdsToCount: type error"); | ||||||
| @ -325,7 +331,7 @@ function findExcerpts(sourceNote: BNote, referencedNoteId: string) { | |||||||
|     return excerpts; |     return excerpts; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getFilteredBacklinks(note: BNote) { | function getFilteredBacklinks(note: BNote): BAttribute[] { | ||||||
|     return ( |     return ( | ||||||
|         note |         note | ||||||
|             .getTargetRelations() |             .getTargetRelations() | ||||||
| @ -344,7 +350,7 @@ function getBacklinkCount(req: Request) { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getBacklinks(req: Request) { | function getBacklinks(req: Request): Backlink[] { | ||||||
|     const { noteId } = req.params; |     const { noteId } = req.params; | ||||||
|     const note = becca.getNoteOrThrow(noteId); |     const note = becca.getNoteOrThrow(noteId); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import fs from "fs"; | |||||||
| import dataDir from "./data_dir.js"; | import dataDir from "./data_dir.js"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
| import resourceDir from "./resource_dir.js"; | import resourceDir from "./resource_dir.js"; | ||||||
|  | import { envToBoolean } from "./utils.js"; | ||||||
| 
 | 
 | ||||||
| const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini"); | const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini"); | ||||||
| 
 | 
 | ||||||
| @ -14,6 +15,79 @@ if (!fs.existsSync(dataDir.CONFIG_INI_PATH)) { | |||||||
|     fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample); |     fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8")); | const iniConfig = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8")); | ||||||
|  | 
 | ||||||
|  | export interface TriliumConfig { | ||||||
|  |     General: { | ||||||
|  |         instanceName: string; | ||||||
|  |         noAuthentication: boolean; | ||||||
|  |         noBackup: boolean; | ||||||
|  |         noDesktopIcon: boolean; | ||||||
|  |     }; | ||||||
|  |     Network: { | ||||||
|  |         host: string; | ||||||
|  |         port: string; | ||||||
|  |         https: boolean; | ||||||
|  |         certPath: string; | ||||||
|  |         keyPath: string; | ||||||
|  |         trustedReverseProxy: boolean | string; | ||||||
|  |     }; | ||||||
|  |     Sync: { | ||||||
|  |         syncServerHost: string; | ||||||
|  |         syncServerTimeout: string; | ||||||
|  |         syncProxy: string; | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //prettier-ignore
 | ||||||
|  | const config: TriliumConfig = { | ||||||
|  | 
 | ||||||
|  |     General: { | ||||||
|  |         instanceName: | ||||||
|  |             process.env.TRILIUM_GENERAL_INSTANCENAME || iniConfig.General.instanceName || "", | ||||||
|  | 
 | ||||||
|  |         noAuthentication:  | ||||||
|  |             envToBoolean(process.env.TRILIUM_GENERAL_NOAUTHENTICATION) || iniConfig.General.noAuthentication || false, | ||||||
|  | 
 | ||||||
|  |         noBackup:  | ||||||
|  |             envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false, | ||||||
|  | 
 | ||||||
|  |         noDesktopIcon:  | ||||||
|  |             envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     Network: { | ||||||
|  |         host: | ||||||
|  |             process.env.TRILIUM_NETWORK_HOST || iniConfig.Network.host || "0.0.0.0", | ||||||
|  | 
 | ||||||
|  |         port: | ||||||
|  |             process.env.TRILIUM_NETWORK_PORT || iniConfig.Network.port || "3000", | ||||||
|  | 
 | ||||||
|  |         https:  | ||||||
|  |             envToBoolean(process.env.TRILIUM_NETWORK_HTTPS) || iniConfig.Network.https || false, | ||||||
|  | 
 | ||||||
|  |         certPath:  | ||||||
|  |             process.env.TRILIUM_NETWORK_CERTPATH || iniConfig.Network.certPath || "", | ||||||
|  | 
 | ||||||
|  |         keyPath:  | ||||||
|  |             process.env.TRILIUM_NETWORK_KEYPATH  || iniConfig.Network.keyPath || "", | ||||||
|  | 
 | ||||||
|  |         trustedReverseProxy: | ||||||
|  |             process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     Sync: { | ||||||
|  |         syncServerHost: | ||||||
|  |             process.env.TRILIUM_SYNC_SERVER_HOST || iniConfig?.Sync?.syncServerHost || "", | ||||||
|  | 
 | ||||||
|  |         syncServerTimeout: | ||||||
|  |             process.env.TRILIUM_SYNC_SERVER_TIMEOUT || iniConfig?.Sync?.syncServerTimeout || "120000", | ||||||
|  | 
 | ||||||
|  |         syncProxy: | ||||||
|  |             // additionally checking in iniConfig for inconsistently named syncProxy for backwards compatibility
 | ||||||
|  |             process.env.TRILIUM_SYNC_SERVER_PROXY || iniConfig?.Sync?.syncProxy || iniConfig?.Sync?.syncServerProxy || "" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export default config; | export default config; | ||||||
|  | |||||||
							
								
								
									
										103
									
								
								src/services/import/utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,103 @@ | |||||||
|  | import { describe, it, expect } from "vitest"; | ||||||
|  | import importUtils from "./utils.js"; | ||||||
|  | 
 | ||||||
|  | type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>]; | ||||||
|  | 
 | ||||||
|  | describe("#extractHtmlTitle", () => { | ||||||
|  |     const htmlWithNoTitle = ` | ||||||
|  |   <html> | ||||||
|  |     <body> | ||||||
|  |       <div>abc</div> | ||||||
|  |     </body> | ||||||
|  |   </html>`;
 | ||||||
|  | 
 | ||||||
|  |     const htmlWithTitle = ` | ||||||
|  |   <html><head> | ||||||
|  |     <title>Test Title</title> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <div>abc</div> | ||||||
|  |   </body> | ||||||
|  |   </html>`;
 | ||||||
|  | 
 | ||||||
|  |     const htmlWithTitleWOpeningBracket = ` | ||||||
|  |   <html><head> | ||||||
|  |   <title>Test < Title</title> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <div>abc</div> | ||||||
|  |   </body> | ||||||
|  |   </html>`;
 | ||||||
|  | 
 | ||||||
|  |     // prettier-ignore
 | ||||||
|  |     const testCases: TestCase<typeof importUtils.extractHtmlTitle>[] = [ | ||||||
|  |         [ | ||||||
|  |             "w/ existing <title> tag, it should return the content of the title tag", | ||||||
|  |             [htmlWithTitle], | ||||||
|  |             "Test Title" | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             // @TriliumNextTODO: this seems more like an unwanted behaviour to me – check if this needs rather fixing
 | ||||||
|  |             "with existing <title> tag, that includes an opening HTML tag '<', it should return null", | ||||||
|  |             [htmlWithTitleWOpeningBracket],  | ||||||
|  |             null | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             "w/o an existing <title> tag, it should reutrn null", | ||||||
|  |             [htmlWithNoTitle], | ||||||
|  |             null | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             "w/ empty string content, it should return null", | ||||||
|  |             [""], | ||||||
|  |             null | ||||||
|  |         ] | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     testCases.forEach((testCase) => { | ||||||
|  |         const [desc, fnParams, expected] = testCase; | ||||||
|  |         return it(desc, () => { | ||||||
|  |             const actual = importUtils.extractHtmlTitle(...fnParams); | ||||||
|  |             expect(actual).toStrictEqual(expected); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | describe("#handleH1", () => { | ||||||
|  |     // prettier-ignore
 | ||||||
|  |     const testCases: TestCase<typeof importUtils.handleH1>[] = [ | ||||||
|  |         [ | ||||||
|  |             "w/ single <h1> tag w/ identical text content as the title tag: the <h1> tag should be stripped", | ||||||
|  |             ["<h1>Title</h1>", "Title"], | ||||||
|  |             "" | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             "w/ multiple <h1> tags, with the fist matching the title tag: the first <h1> tag should be stripped and subsequent tags converted to <h2>", | ||||||
|  |             ["<h1>Title</h1><h1>Header 1</h1><h1>Header 2</h1>", "Title"], | ||||||
|  |             "<h2>Header 1</h2><h2>Header 2</h2>" | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             "w/ no <h1> tag and only <h2> tags, it should not cause any changes and return the same content", | ||||||
|  |             ["<h2>Heading 1</h2><h2>Heading 2</h2>", "Title"], | ||||||
|  |             "<h2>Heading 1</h2><h2>Heading 2</h2>" | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             "w/ multiple <h1> tags, and the 1st matching the title tag, it should strip ONLY the very first occurence of the <h1> tags in the returned content", | ||||||
|  |             ["<h1>Topic ABC</h1><h1>Heading 1</h1><h1>Topic ABC</h1>", "Topic ABC"], | ||||||
|  |             "<h2>Heading 1</h2><h2>Topic ABC</h2>" | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             "w/ multiple <h1> tags, and the 1st matching NOT the title tag, it should NOT strip any other <h1> tags", | ||||||
|  |             ["<h1>Introduction</h1><h1>Topic ABC</h1><h1>Summary</h1>", "Topic ABC"], | ||||||
|  |             "<h2>Introduction</h2><h2>Topic ABC</h2><h2>Summary</h2>" | ||||||
|  |         ] | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     testCases.forEach((testCase) => { | ||||||
|  |         const [desc, fnParams, expected] = testCase; | ||||||
|  |         return it(desc, () => { | ||||||
|  |             const actual = importUtils.handleH1(...fnParams); | ||||||
|  |             expect(actual).toStrictEqual(expected); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
| @ -1,14 +1,19 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
| 
 | 
 | ||||||
| function handleH1(content: string, title: string) { | function handleH1(content: string, title: string) { | ||||||
|     content = content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => { |     let isFirstH1Handled = false; | ||||||
|         if (title.trim() === text.trim()) { | 
 | ||||||
|             return ""; // remove whole H1 tag
 |     return content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => { | ||||||
|         } else { |         const convertedContent = `<h2>${text}</h2>`; | ||||||
|             return `<h2>${text}</h2>`; | 
 | ||||||
|  |         // strip away very first found h1 tag, if it matches the title
 | ||||||
|  |         if (!isFirstH1Handled) { | ||||||
|  |             isFirstH1Handled = true; | ||||||
|  |             return title.trim() === text.trim() ? "" : convertedContent; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         return convertedContent; | ||||||
|     }); |     }); | ||||||
|     return content; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function extractHtmlTitle(content: string): string | null { | function extractHtmlTitle(content: string): string | null { | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ function getRunAtHours(note: BNote): number[] { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function runNotesWithLabel(runAttrValue: string) { | function runNotesWithLabel(runAttrValue: string) { | ||||||
|     const instanceName = config.General ? config.General.instanceName : null; |     const instanceName = config.General.instanceName; | ||||||
|     const currentHours = new Date().getHours(); |     const currentHours = new Date().getHours(); | ||||||
|     const notes = attributeService.getNotesWithLabel("run", runAttrValue); |     const notes = attributeService.getNotesWithLabel("run", runAttrValue); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
| 
 | 
 | ||||||
| import optionService from "./options.js"; | import optionService from "./options.js"; | ||||||
| import type { OptionNames } from "./options_interface.js"; |  | ||||||
| import config from "./config.js"; | import config from "./config.js"; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
| @ -11,14 +10,14 @@ import config from "./config.js"; | |||||||
|  * to live sync server. |  * to live sync server. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| function get(name: OptionNames) { | function get(name: keyof typeof config.Sync) { | ||||||
|   return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name); |   return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|     // env variable is the easiest way to guarantee we won't overwrite prod data during development
 |     // env variable is the easiest way to guarantee we won't overwrite prod data during development
 | ||||||
|     // after copying prod document/data directory
 |     // after copying prod document/data directory
 | ||||||
|     getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get("syncServerHost"), |     getSyncServerHost: () => get("syncServerHost"), | ||||||
|     isSyncSetup: () => { |     isSyncSetup: () => { | ||||||
|         const syncServerHost = get("syncServerHost"); |         const syncServerHost = get("syncServerHost"); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -295,6 +295,18 @@ export function isString(x: any) { | |||||||
|     return Object.prototype.toString.call(x) === "[object String]"; |     return Object.prototype.toString.call(x) === "[object String]"; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
 | ||||||
|  | export function envToBoolean(val: string | undefined) { | ||||||
|  |     if (val === undefined || typeof val !== "string") return undefined; | ||||||
|  | 
 | ||||||
|  |     const valLc = val.toLowerCase().trim(); | ||||||
|  | 
 | ||||||
|  |     if (valLc === "true") return true; | ||||||
|  |     if (valLc === "false") return false; | ||||||
|  | 
 | ||||||
|  |     return undefined; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package. |  * Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package. | ||||||
|  * On development builds, this simply refers to the root directory of the application. |  * On development builds, this simply refers to the root directory of the application. | ||||||
| @ -352,5 +364,6 @@ export default { | |||||||
|     isString, |     isString, | ||||||
|     getResourceDir, |     getResourceDir, | ||||||
|     isMac, |     isMac, | ||||||
|     isWindows |     isWindows, | ||||||
|  |     envToBoolean | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import WebSocket from "ws"; | import { WebSocketServer as WebSocketServer, WebSocket } from "ws"; | ||||||
| import { isElectron, randomString } from "./utils.js"; | import { isElectron, randomString } from "./utils.js"; | ||||||
| import log from "./log.js"; | import log from "./log.js"; | ||||||
| import sql from "./sql.js"; | import sql from "./sql.js"; | ||||||
| @ -10,7 +10,7 @@ import becca from "../becca/becca.js"; | |||||||
| import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js"; | import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js"; | ||||||
| 
 | 
 | ||||||
| import env from "./env.js"; | import env from "./env.js"; | ||||||
| import type { IncomingMessage, Server } from "http"; | import type { IncomingMessage, Server as HttpServer } from "http"; | ||||||
| import type { EntityChange } from "./entity_changes_interface.js"; | import type { EntityChange } from "./entity_changes_interface.js"; | ||||||
| 
 | 
 | ||||||
| if (env.isDev()) { | if (env.isDev()) { | ||||||
| @ -24,7 +24,7 @@ if (env.isDev()) { | |||||||
|         .on("unlink", debouncedReloadFrontend); |         .on("unlink", debouncedReloadFrontend); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| let webSocketServer!: WebSocket.Server; | let webSocketServer!: WebSocketServer; | ||||||
| let lastSyncedPush: number | null = null; | let lastSyncedPush: number | null = null; | ||||||
| 
 | 
 | ||||||
| interface Message { | interface Message { | ||||||
| @ -58,8 +58,8 @@ interface Message { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void; | type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void; | ||||||
| function init(httpServer: Server, sessionParser: SessionParser) { | function init(httpServer: HttpServer, sessionParser: SessionParser) { | ||||||
|     webSocketServer = new WebSocket.Server({ |     webSocketServer = new WebSocketServer({ | ||||||
|         verifyClient: (info, done) => { |         verifyClient: (info, done) => { | ||||||
|             sessionParser(info.req, {}, () => { |             sessionParser(info.req, {}, () => { | ||||||
|                 const allowed = isElectron() || (info.req as any).session.loggedIn || (config.General && config.General.noAuthentication); |                 const allowed = isElectron() || (info.req as any).session.loggedIn || (config.General && config.General.noAuthentication); | ||||||
|  | |||||||
| @ -1,14 +1,16 @@ | |||||||
| import { fileURLToPath } from "url"; | import { fileURLToPath } from "url"; | ||||||
| import path from "path"; | import path from "path"; | ||||||
| import assetPath from "./src/services/asset_path.js"; | import assetPath from "./src/services/asset_path.js"; | ||||||
|  | import type { Configuration } from "webpack"; | ||||||
| 
 | 
 | ||||||
| const rootDir = path.dirname(fileURLToPath(import.meta.url)); | const rootDir = path.dirname(fileURLToPath(import.meta.url)); | ||||||
| export default { | const config: Configuration = { | ||||||
|     mode: "production", |     mode: "production", | ||||||
|     entry: { |     entry: { | ||||||
|         setup: "./src/public/app/setup.js", |         setup: "./src/public/app/setup.js", | ||||||
|         mobile: "./src/public/app/mobile.js", |         mobile: "./src/public/app/mobile.js", | ||||||
|         desktop: "./src/public/app/desktop.js" |         desktop: "./src/public/app/desktop.js", | ||||||
|  |         share: "./src/public/app/share.js" | ||||||
|     }, |     }, | ||||||
|     output: { |     output: { | ||||||
|         publicPath: `${assetPath}/app-dist/`, |         publicPath: `${assetPath}/app-dist/`, | ||||||
| @ -42,3 +44,5 @@ export default { | |||||||
|     devtool: "source-map", |     devtool: "source-map", | ||||||
|     target: "electron-renderer" |     target: "electron-renderer" | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export default config; | ||||||
 Adorian Doran
						Adorian Doran