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
 | 
			
		||||
description: Report a bug
 | 
			
		||||
title: "(Bug report) "
 | 
			
		||||
labels: "Type: Bug"
 | 
			
		||||
type: "Bug"
 | 
			
		||||
body:
 | 
			
		||||
- type: textarea
 | 
			
		||||
  attributes:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/feature_request.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,12 +1,11 @@
 | 
			
		||||
name: Feature Request
 | 
			
		||||
description: Ask for a new feature to be added
 | 
			
		||||
title: "(Feature request) "
 | 
			
		||||
labels: "Type: Enhancement"
 | 
			
		||||
type: "Feature"
 | 
			
		||||
body:
 | 
			
		||||
- type: textarea
 | 
			
		||||
  attributes:
 | 
			
		||||
    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:
 | 
			
		||||
    required: true
 | 
			
		||||
- type: textarea
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -92,7 +92,16 @@ jobs:
 | 
			
		||||
          asset_content_type: application/zip # required by GitHub API
 | 
			
		||||
  nightly-server:
 | 
			
		||||
    name: Deploy server nightly
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        arch: [x64, arm64]
 | 
			
		||||
        include:
 | 
			
		||||
          - arch: x64
 | 
			
		||||
            runs-on: ubuntu-latest
 | 
			
		||||
          - arch: arm64
 | 
			
		||||
            runs-on: ubuntu-24.04-arm
 | 
			
		||||
    runs-on: ${{ matrix.runs-on }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
@@ -102,22 +111,21 @@ jobs:
 | 
			
		||||
          cache: "npm"
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: npm ci
 | 
			
		||||
      - name: Run Linux server build (x86_64)
 | 
			
		||||
      - name: Run Linux server build
 | 
			
		||||
        env:
 | 
			
		||||
          MATRIX_ARCH: ${{ matrix.arch }}
 | 
			
		||||
        run: |
 | 
			
		||||
          npm run update-build-info
 | 
			
		||||
          npm run ci-update-nightly-version
 | 
			
		||||
          ./bin/build-server.sh
 | 
			
		||||
      - name: Prepare artifacts
 | 
			
		||||
        if: runner.os != 'windows'
 | 
			
		||||
        run: |
 | 
			
		||||
          mkdir -p upload
 | 
			
		||||
          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
 | 
			
		||||
        with:
 | 
			
		||||
          name: TriliumNextNotes linux server x64
 | 
			
		||||
          path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
 | 
			
		||||
          overwrite: true
 | 
			
		||||
          name: TriliumNextNotes linux server ${{ matrix.arch }}
 | 
			
		||||
          path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
 | 
			
		||||
 | 
			
		||||
      - name: Deploy release
 | 
			
		||||
        uses: WebFreak001/deploy-nightly@v3.2.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -66,8 +66,17 @@ jobs:
 | 
			
		||||
          fail_on_unmatched_files: true
 | 
			
		||||
          files: upload/*.*
 | 
			
		||||
  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
 | 
			
		||||
          - arch: arm64
 | 
			
		||||
            runs-on: ubuntu-24.04-arm
 | 
			
		||||
    runs-on: ${{ matrix.runs-on }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
@@ -77,16 +86,17 @@ jobs:
 | 
			
		||||
          cache: "npm"
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: npm ci
 | 
			
		||||
      - name: Run Linux server build (x86_64)
 | 
			
		||||
      - name: Run Linux server build
 | 
			
		||||
        env:
 | 
			
		||||
          MATRIX_ARCH: ${{ matrix.arch }}
 | 
			
		||||
        run: |
 | 
			
		||||
          npm run update-build-info
 | 
			
		||||
          ./bin/build-server.sh
 | 
			
		||||
      - name: Prepare artifacts
 | 
			
		||||
        if: runner.os != 'windows'
 | 
			
		||||
        run: |
 | 
			
		||||
          mkdir -p upload
 | 
			
		||||
          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
 | 
			
		||||
        uses: softprops/action-gh-release@v2
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# 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
 | 
			
		||||
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 prune --omit=dev && \
 | 
			
		||||
    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/. && \
 | 
			
		||||
    rm -rf src/public/app/* && \
 | 
			
		||||
    mkdir -p src/public/app/services && \
 | 
			
		||||
@@ -37,7 +36,7 @@ RUN cp -R build/src/* src/. && \
 | 
			
		||||
    rm -r build
 | 
			
		||||
 | 
			
		||||
# Runtime stage
 | 
			
		||||
FROM node:22.13.0-bullseye-slim
 | 
			
		||||
FROM node:22.13.1-bullseye-slim
 | 
			
		||||
 | 
			
		||||
# Install only runtime dependencies
 | 
			
		||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# Build stage
 | 
			
		||||
FROM node:22.13.0-alpine AS builder
 | 
			
		||||
FROM node:22.13.1-alpine AS builder
 | 
			
		||||
 | 
			
		||||
# Configure build dependencies
 | 
			
		||||
RUN apk add --no-cache --virtual .build-dependencies \
 | 
			
		||||
@@ -27,7 +27,6 @@ RUN cp -R build/src/* src/. && \
 | 
			
		||||
    npm run webpack && \
 | 
			
		||||
    npm prune --omit=dev && \
 | 
			
		||||
    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/. && \
 | 
			
		||||
    rm -rf src/public/app && \
 | 
			
		||||
    mkdir -p src/public/app/services && \
 | 
			
		||||
@@ -36,7 +35,7 @@ RUN cp -R build/src/* src/. && \
 | 
			
		||||
    rm -r build
 | 
			
		||||
 | 
			
		||||
# Runtime stage
 | 
			
		||||
FROM node:22.13.0-alpine
 | 
			
		||||
FROM node:22.13.1-alpine
 | 
			
		||||
 | 
			
		||||
# Install runtime dependencies
 | 
			
		||||
RUN apk add --no-cache su-exec shadow
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,6 @@ find $DIR -name "*.ts" -type f -delete
 | 
			
		||||
 | 
			
		||||
d="$DIR"/src/public
 | 
			
		||||
[[ -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/
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
# 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
 | 
			
		||||
 | 
			
		||||
[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 ini from "ini";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import dataDir from "./src/services/data_dir.js";
 | 
			
		||||
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
 | 
			
		||||
import config from "./src/services/config.js";
 | 
			
		||||
 | 
			
		||||
if (config.Network.https) {
 | 
			
		||||
    // 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",
 | 
			
		||||
  "productName": "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",
 | 
			
		||||
  "main": "./dist/electron-main.js",
 | 
			
		||||
  "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",
 | 
			
		||||
    "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-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-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",
 | 
			
		||||
    "switch-server": "rimraf ./node_modules/better-sqlite3 && npm install",
 | 
			
		||||
    "switch-electron": "electron-rebuild",
 | 
			
		||||
@@ -59,7 +59,7 @@
 | 
			
		||||
    "@excalidraw/excalidraw": "0.17.6",
 | 
			
		||||
    "@highlightjs/cdn-assets": "11.11.1",
 | 
			
		||||
    "@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",
 | 
			
		||||
    "@types/leaflet": "1.9.16",
 | 
			
		||||
    "@types/react-dom": "18.3.5",
 | 
			
		||||
@@ -97,9 +97,9 @@
 | 
			
		||||
    "html2plaintext": "2.1.4",
 | 
			
		||||
    "http-proxy-agent": "7.0.2",
 | 
			
		||||
    "https-proxy-agent": "7.0.6",
 | 
			
		||||
    "i18next": "24.2.1",
 | 
			
		||||
    "i18next": "24.2.2",
 | 
			
		||||
    "i18next-fs-backend": "2.6.0",
 | 
			
		||||
    "i18next-http-backend": "3.0.1",
 | 
			
		||||
    "i18next-http-backend": "3.0.2",
 | 
			
		||||
    "image-type": "5.2.0",
 | 
			
		||||
    "ini": "5.0.0",
 | 
			
		||||
    "is-animated": "2.0.2",
 | 
			
		||||
@@ -148,14 +148,14 @@
 | 
			
		||||
    "yauzl": "3.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@electron-forge/cli": "7.6.0",
 | 
			
		||||
    "@electron-forge/maker-deb": "7.6.0",
 | 
			
		||||
    "@electron-forge/maker-dmg": "7.6.0",
 | 
			
		||||
    "@electron-forge/maker-squirrel": "7.6.0",
 | 
			
		||||
    "@electron-forge/maker-zip": "7.6.0",
 | 
			
		||||
    "@electron-forge/plugin-auto-unpack-natives": "7.6.0",
 | 
			
		||||
    "@electron-forge/cli": "7.6.1",
 | 
			
		||||
    "@electron-forge/maker-deb": "7.6.1",
 | 
			
		||||
    "@electron-forge/maker-dmg": "7.6.1",
 | 
			
		||||
    "@electron-forge/maker-squirrel": "7.6.1",
 | 
			
		||||
    "@electron-forge/maker-zip": "7.6.1",
 | 
			
		||||
    "@electron-forge/plugin-auto-unpack-natives": "7.6.1",
 | 
			
		||||
    "@electron/rebuild": "3.7.1",
 | 
			
		||||
    "@playwright/test": "1.49.1",
 | 
			
		||||
    "@playwright/test": "1.50.0",
 | 
			
		||||
    "@types/archiver": "6.0.3",
 | 
			
		||||
    "@types/better-sqlite3": "7.6.12",
 | 
			
		||||
    "@types/bootstrap": "5.2.10",
 | 
			
		||||
@@ -177,7 +177,7 @@
 | 
			
		||||
    "@types/jsdom": "21.1.7",
 | 
			
		||||
    "@types/mime-types": "2.1.4",
 | 
			
		||||
    "@types/multer": "1.4.12",
 | 
			
		||||
    "@types/node": "22.10.7",
 | 
			
		||||
    "@types/node": "22.12.0",
 | 
			
		||||
    "@types/react": "18.3.18",
 | 
			
		||||
    "@types/safe-compare": "1.1.2",
 | 
			
		||||
    "@types/sanitize-html": "2.13.0",
 | 
			
		||||
@@ -189,12 +189,12 @@
 | 
			
		||||
    "@types/stream-throttle": "0.1.4",
 | 
			
		||||
    "@types/tmp": "0.2.6",
 | 
			
		||||
    "@types/turndown": "5.0.5",
 | 
			
		||||
    "@types/ws": "8.5.13",
 | 
			
		||||
    "@types/ws": "8.5.14",
 | 
			
		||||
    "@types/xml2js": "0.4.14",
 | 
			
		||||
    "@types/yargs": "17.0.33",
 | 
			
		||||
    "@vitest/coverage-v8": "3.0.3",
 | 
			
		||||
    "@vitest/coverage-v8": "3.0.4",
 | 
			
		||||
    "cross-env": "7.0.3",
 | 
			
		||||
    "electron": "34.0.0",
 | 
			
		||||
    "electron": "34.0.1",
 | 
			
		||||
    "esm": "3.2.25",
 | 
			
		||||
    "jasmine": "5.5.0",
 | 
			
		||||
    "jsdoc": "4.0.4",
 | 
			
		||||
@@ -207,7 +207,7 @@
 | 
			
		||||
    "tsx": "4.19.2",
 | 
			
		||||
    "typedoc": "0.27.6",
 | 
			
		||||
    "typescript": "5.7.3",
 | 
			
		||||
    "vitest": "3.0.3",
 | 
			
		||||
    "vitest": "3.0.4",
 | 
			
		||||
    "webpack": "5.97.1",
 | 
			
		||||
    "webpack-cli": "6.0.1",
 | 
			
		||||
    "webpack-dev-middleware": "7.4.2"
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,12 @@ interface DateLimits {
 | 
			
		||||
    maxDate: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface SimilarNote {
 | 
			
		||||
    score: number;
 | 
			
		||||
    notePath: string[];
 | 
			
		||||
    noteId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function filterUrlValue(value: string) {
 | 
			
		||||
    return value
 | 
			
		||||
        .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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function findSimilarNotes(noteId: string) {
 | 
			
		||||
async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefined> {
 | 
			
		||||
    const results = [];
 | 
			
		||||
    let i = 0;
 | 
			
		||||
 | 
			
		||||
@@ -417,6 +423,7 @@ async function findSimilarNotes(noteId: string) {
 | 
			
		||||
 | 
			
		||||
            // this takes care of note hoisting
 | 
			
		||||
            if (!notePath) {
 | 
			
		||||
                // TODO: This return is suspicious, it should probably be continue
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ export interface ExecuteCommandData extends CommandData {
 | 
			
		||||
export type CommandMappings = {
 | 
			
		||||
    "api-log-messages": CommandData;
 | 
			
		||||
    focusTree: CommandData,
 | 
			
		||||
    focusOnDetail: Required<CommandData>;
 | 
			
		||||
    focusOnDetail: CommandData;
 | 
			
		||||
    focusOnSearchDefinition: Required<CommandData>;
 | 
			
		||||
    searchNotes: CommandData & {
 | 
			
		||||
        searchString?: string;
 | 
			
		||||
@@ -104,6 +104,8 @@ export type CommandMappings = {
 | 
			
		||||
    openNoteInNewTab: CommandData;
 | 
			
		||||
    openNoteInNewSplit: CommandData;
 | 
			
		||||
    openNoteInNewWindow: CommandData;
 | 
			
		||||
    hideLeftPane: CommandData;
 | 
			
		||||
    showLeftPane: CommandData;
 | 
			
		||||
 | 
			
		||||
    openInTab: ContextMenuCommandData;
 | 
			
		||||
    openNoteInSplit: ContextMenuCommandData;
 | 
			
		||||
@@ -236,6 +238,9 @@ type EventMappings = {
 | 
			
		||||
    beforeNoteSwitch: {
 | 
			
		||||
        noteContext: NoteContext;
 | 
			
		||||
    };
 | 
			
		||||
    beforeNoteContextRemove: {
 | 
			
		||||
        ntxIds: string[];
 | 
			
		||||
    };
 | 
			
		||||
    noteSwitched: {
 | 
			
		||||
        noteContext: NoteContext;
 | 
			
		||||
        notePath: string | null;
 | 
			
		||||
@@ -286,6 +291,9 @@ type EventMappings = {
 | 
			
		||||
    tabReorder: {
 | 
			
		||||
        ntxIdsInOrder: string[]
 | 
			
		||||
    };
 | 
			
		||||
    refreshNoteList: {
 | 
			
		||||
        noteId: string;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type EventListener<T extends EventNames> = {
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
 | 
			
		||||
        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 {
 | 
			
		||||
            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 { t } from "./services/i18n.js";
 | 
			
		||||
import options from "./services/options.js";
 | 
			
		||||
import type ElectronRemote from "@electron/remote";
 | 
			
		||||
import type Electron from "electron";
 | 
			
		||||
 | 
			
		||||
await appContext.earlyInit();
 | 
			
		||||
 | 
			
		||||
@@ -44,10 +46,9 @@ if (utils.isElectron()) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initOnElectron() {
 | 
			
		||||
    const electron = utils.dynamicRequire("electron");
 | 
			
		||||
    const electron: typeof Electron = utils.dynamicRequire("electron");
 | 
			
		||||
    electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
 | 
			
		||||
 | 
			
		||||
    const electronRemote = utils.dynamicRequire("@electron/remote");
 | 
			
		||||
    const electronRemote: typeof ElectronRemote = utils.dynamicRequire("@electron/remote");
 | 
			
		||||
    const currentWindow = electronRemote.getCurrentWindow();
 | 
			
		||||
    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") {
 | 
			
		||||
        const applyWindowsOverlay = () => {
 | 
			
		||||
            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") {
 | 
			
		||||
        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
 | 
			
		||||
 * 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;
 | 
			
		||||
    isInHoistedSubTree: boolean;
 | 
			
		||||
    isSearch: boolean;
 | 
			
		||||
    isSearch?: boolean;
 | 
			
		||||
    notePath: string[];
 | 
			
		||||
    isHidden: boolean;
 | 
			
		||||
}
 | 
			
		||||
@@ -402,14 +402,14 @@ class FNote {
 | 
			
		||||
        return notePaths;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getSortedNotePathRecords(hoistedNoteId = "root") {
 | 
			
		||||
    getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
 | 
			
		||||
        const isHoistedRoot = hoistedNoteId === "root";
 | 
			
		||||
 | 
			
		||||
        const notePaths = this.getAllNotePaths().map((path) => ({
 | 
			
		||||
        const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
 | 
			
		||||
            notePath: path,
 | 
			
		||||
            isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
 | 
			
		||||
            isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
 | 
			
		||||
            isSearch: path.find((noteId) => froca.notes[noteId].type === "search"),
 | 
			
		||||
            isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
 | 
			
		||||
            isHidden: path.includes("_hidden")
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ interface NoteRow {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface BranchRow {
 | 
			
		||||
    noteId?: string;
 | 
			
		||||
    branchId: string;
 | 
			
		||||
    componentId: string;
 | 
			
		||||
    parentNoteId?: string;
 | 
			
		||||
@@ -157,7 +158,7 @@ export default class LoadResults {
 | 
			
		||||
        return Object.keys(this.noteIdToComponentId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isNoteReloaded(noteId: string, componentId = null) {
 | 
			
		||||
    isNoteReloaded(noteId: string | undefined, componentId: string | null = null) {
 | 
			
		||||
        if (!noteId) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -124,6 +124,10 @@ function escapeHtml(str: string) {
 | 
			
		||||
    return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function escapeQuotes(value: string) {
 | 
			
		||||
    return value.replaceAll("\"", """);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatSize(size: number) {
 | 
			
		||||
    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.
 | 
			
		||||
 */
 | 
			
		||||
async function fetchNote(noteId = null) {
 | 
			
		||||
async function fetchNote(noteId: string | null = null) {
 | 
			
		||||
    if (!noteId) {
 | 
			
		||||
        noteId = document.body.getAttribute("data-note-id");
 | 
			
		||||
    }
 | 
			
		||||
@@ -25,3 +25,9 @@ document.addEventListener(
 | 
			
		||||
    },
 | 
			
		||||
    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[];
 | 
			
		||||
    triliumVersion: string;
 | 
			
		||||
    TRILIUM_SAFE_MODE: boolean;
 | 
			
		||||
    platform?: typeof process.platform;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
 | 
			
		||||
import type FNote from "../../entities/fnote.js";
 | 
			
		||||
import { escapeQuotes } from "../../services/utils.js";
 | 
			
		||||
 | 
			
		||||
const HELP_TEXT = `
 | 
			
		||||
<p>${t("attribute_editor.help_text_body1")}</p>
 | 
			
		||||
@@ -76,8 +77,8 @@ const TPL = `
 | 
			
		||||
 | 
			
		||||
    <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-plus add-new-attribute-button" title="${t("attribute_editor.add_a_new_attribute")}"></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="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
 | 
			
		||||
 | 
			
		||||
    <div class="attribute-errors" style="display: none;"></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.
 | 
			
		||||
     * @returns whether the widget is enabled.
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
    isEnabled(): boolean | null | undefined {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -205,7 +205,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
 | 
			
		||||
     */
 | 
			
		||||
    doRender() {}
 | 
			
		||||
 | 
			
		||||
    toggleInt(show: boolean) {
 | 
			
		||||
    toggleInt(show: boolean | null | undefined) {
 | 
			
		||||
        this.$widget.toggleClass("hidden-int", !show);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,11 @@ import options from "../../services/options.js";
 | 
			
		||||
import splitService from "../../services/resizer.js";
 | 
			
		||||
import CommandButtonWidget from "./command_button.js";
 | 
			
		||||
import { t } from "../../services/i18n.js";
 | 
			
		||||
import type { EventData } from "../../components/app_context.js";
 | 
			
		||||
 | 
			
		||||
export default class LeftPaneToggleWidget extends CommandButtonWidget {
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
 | 
			
		||||
    constructor(isHorizontalLayout: boolean) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
 | 
			
		||||
@@ -32,7 +34,7 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
 | 
			
		||||
        splitService.setupLeftPaneResizer(options.is("leftPaneVisible"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (loadResults.isOptionReloaded("leftPaneVisible")) {
 | 
			
		||||
            this.refreshIcon();
 | 
			
		||||
        }
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
			
		||||
import keyboardActionsService from "../../services/keyboard_actions.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 = `
 | 
			
		||||
<div class="ribbon-container">
 | 
			
		||||
@@ -113,6 +117,16 @@ const TPL = `
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
@@ -122,10 +136,10 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
        this.ribbonWidgets.push(widget);
 | 
			
		||||
@@ -133,7 +147,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    button(widget) {
 | 
			
		||||
    button(widget: CommandButtonWidget) {
 | 
			
		||||
        super.child(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");
 | 
			
		||||
 | 
			
		||||
        this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
 | 
			
		||||
@@ -181,14 +195,15 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
            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 });
 | 
			
		||||
 | 
			
		||||
                if (refreshActiveTab) {
 | 
			
		||||
                    if (handleEventPromise) {
 | 
			
		||||
                        handleEventPromise.then(() => activeChild.focus?.());
 | 
			
		||||
                        handleEventPromise.then(() => (activeChild as any).focus()); // TODO: Base class
 | 
			
		||||
                    } else {
 | 
			
		||||
                        activeChild.focus?.();
 | 
			
		||||
                        // TODO: Base class
 | 
			
		||||
                        (activeChild as any)?.focus();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -203,7 +218,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        await super.noteSwitched();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshWithNote(note, noExplicitActivation = false) {
 | 
			
		||||
    async refreshWithNote(note: FNote, noExplicitActivation = false) {
 | 
			
		||||
        this.lastNoteType = note.type;
 | 
			
		||||
 | 
			
		||||
        let $ribbonTabToActivate, $lastActiveRibbon;
 | 
			
		||||
@@ -211,7 +226,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        this.$tabContainer.empty();
 | 
			
		||||
 | 
			
		||||
        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) {
 | 
			
		||||
                continue;
 | 
			
		||||
@@ -219,8 +235,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
            const $ribbonTitle = $('<div class="ribbon-tab-title">')
 | 
			
		||||
                .attr("data-ribbon-component-id", ribbonWidget.componentId)
 | 
			
		||||
                .attr("data-ribbon-component-name", ribbonWidget.name)
 | 
			
		||||
                .append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", ribbonWidget.toggleCommand))
 | 
			
		||||
                .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 as any).toggleCommand)) // TODO: base class
 | 
			
		||||
                .append(" ")
 | 
			
		||||
                .append($('<span class="ribbon-tab-title-label">').text(ret.title));
 | 
			
		||||
 | 
			
		||||
@@ -238,7 +254,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        keyboardActionsService.getActions().then((actions) => {
 | 
			
		||||
            this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
 | 
			
		||||
                title: function () {
 | 
			
		||||
                title: () => {
 | 
			
		||||
                    const toggleCommandName = $(this).attr("data-toggle-command");
 | 
			
		||||
                    const action = actions.find((act) => act.actionName === toggleCommandName);
 | 
			
		||||
                    const title = $(this).attr("data-title");
 | 
			
		||||
@@ -246,7 +262,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
                    if (action && action.effectiveShortcuts.length > 0) {
 | 
			
		||||
                        return `${title} (${action.effectiveShortcuts.join(", ")})`;
 | 
			
		||||
                    } 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}']`);
 | 
			
		||||
 | 
			
		||||
        return $ribbonComponent.hasClass("active");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ensureOwnedAttributesAreOpen(ntxId) {
 | 
			
		||||
        if (this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
 | 
			
		||||
    ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
 | 
			
		||||
        if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
 | 
			
		||||
            this.toggleRibbonTabWithName("ownedAttributes", ntxId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addNewLabelEvent({ ntxId }) {
 | 
			
		||||
    addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
 | 
			
		||||
        this.ensureOwnedAttributesAreOpen(ntxId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addNewRelationEvent({ ntxId }) {
 | 
			
		||||
    addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
 | 
			
		||||
        this.ensureOwnedAttributesAreOpen(ntxId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toggleRibbonTabWithName(name, ntxId) {
 | 
			
		||||
    toggleRibbonTabWithName(name: string, ntxId?: string) {
 | 
			
		||||
        if (!this.isNoteContext(ntxId)) {
 | 
			
		||||
            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";
 | 
			
		||||
 | 
			
		||||
        if (name.startsWith(PREFIX)) {
 | 
			
		||||
            let componentName = name.substr(PREFIX.length);
 | 
			
		||||
            componentName = componentName[0].toLowerCase() + componentName.substr(1);
 | 
			
		||||
 | 
			
		||||
            this.toggleRibbonTabWithName(componentName, data.ntxId);
 | 
			
		||||
            this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
 | 
			
		||||
        } else {
 | 
			
		||||
            return super.handleEvent(name, data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleEventInChildren(name, data) {
 | 
			
		||||
    async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
 | 
			
		||||
        if (["activeContextChanged", "setNoteContext"].includes(name)) {
 | 
			
		||||
            // won't trigger .refresh();
 | 
			
		||||
            await super.handleEventInChildren("setNoteContext", data);
 | 
			
		||||
            await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
 | 
			
		||||
        } else if (this.isEnabled() || name === "initialRenderComplete") {
 | 
			
		||||
            const activeRibbonWidget = this.getActiveRibbonWidget();
 | 
			
		||||
 | 
			
		||||
@@ -326,8 +342,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
        if (loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        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
 | 
			
		||||
            // check for the type is so that we don't update on each title rename
 | 
			
		||||
            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
 | 
			
		||||
        // 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 importService from "../../services/import.js";
 | 
			
		||||
import options from "../../services/options.js";
 | 
			
		||||
@@ -27,21 +27,21 @@ const TPL = `
 | 
			
		||||
                        <strong>${t("import.options")}:</strong>
 | 
			
		||||
 | 
			
		||||
                        <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>
 | 
			
		||||
                                <span>${t("import.safeImport")}</span>
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <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>
 | 
			
		||||
                                <span>${t("import.explodeArchives")}</span>
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <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>
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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 importService from "../../services/import.js";
 | 
			
		||||
import options from "../../services/options.js";
 | 
			
		||||
@@ -24,7 +24,7 @@ const TPL = `
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <strong>${t("upload_attachments.options")}:</strong>
 | 
			
		||||
                        <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>
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import attributeService from "../services/attributes.js";
 | 
			
		||||
import NoteContextAwareWidget from "./note_context_aware_widget.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 = `
 | 
			
		||||
<div class="dropdown editability-select-widget">
 | 
			
		||||
@@ -9,13 +13,17 @@ const TPL = `
 | 
			
		||||
        width: 300px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .editability-dropdown .dropdown-item {
 | 
			
		||||
        display: block !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .editability-dropdown .dropdown-item div {
 | 
			
		||||
        font-size: small;
 | 
			
		||||
        color: var(--muted-text-color);
 | 
			
		||||
        white-space: normal;
 | 
			
		||||
    }
 | 
			
		||||
    </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="caret"></span>
 | 
			
		||||
    </button>
 | 
			
		||||
@@ -40,9 +48,15 @@ const TPL = `
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private dropdown!: bootstrap.Dropdown;
 | 
			
		||||
    private $editabilityActiveDesc!: JQuery<HTMLElement>;
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
        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.$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");
 | 
			
		||||
 | 
			
		||||
            if (!this.note || !this.noteId) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const ownedAttr of this.note.getOwnedLabels()) {
 | 
			
		||||
                if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
 | 
			
		||||
                    await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (editability !== "auto") {
 | 
			
		||||
            if (editability && editability !== "auto") {
 | 
			
		||||
                await attributeService.addLabel(this.noteId, editability);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshWithNote(note) {
 | 
			
		||||
        let editability = "auto";
 | 
			
		||||
    async refreshWithNote(note: FNote) {
 | 
			
		||||
        let editability: Editability = "auto";
 | 
			
		||||
 | 
			
		||||
        if (this.note.isLabelTruthy("readOnly")) {
 | 
			
		||||
        if (this.note?.isLabelTruthy("readOnly")) {
 | 
			
		||||
            editability = "readOnly";
 | 
			
		||||
        } else if (this.note.isLabelTruthy("autoReadOnlyDisabled")) {
 | 
			
		||||
        } else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
 | 
			
		||||
            editability = "autoReadOnlyDisabled";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -85,7 +103,7 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.$editabilityActiveDesc.text(labels[editability]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
 | 
			
		||||
            this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
@@ -7,6 +7,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
			
		||||
import linkService from "../../services/link.js";
 | 
			
		||||
import server from "../../services/server.js";
 | 
			
		||||
import froca from "../../services/froca.js";
 | 
			
		||||
import type FNote from "../../entities/fnote.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="backlinks-widget">
 | 
			
		||||
@@ -64,7 +65,19 @@ const TPL = `
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// TODO: Deduplicate with server
 | 
			
		||||
interface Backlink {
 | 
			
		||||
    noteId: string;
 | 
			
		||||
    relationName?: string;
 | 
			
		||||
    excerpts?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private $count!: JQuery<HTMLElement>;
 | 
			
		||||
    private $items!: JQuery<HTMLElement>;
 | 
			
		||||
    private $ticker!: JQuery<HTMLElement>;
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$count = this.$widget.find(".backlinks-count");
 | 
			
		||||
@@ -73,7 +86,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        this.$count.on("click", () => {
 | 
			
		||||
            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")) {
 | 
			
		||||
                this.renderBacklinks();
 | 
			
		||||
@@ -83,7 +96,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.contentSized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshWithNote(note) {
 | 
			
		||||
    async refreshWithNote(note: FNote) {
 | 
			
		||||
        this.clearItems();
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        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) {
 | 
			
		||||
            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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -121,7 +135,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        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) {
 | 
			
		||||
            return;
 | 
			
		||||
@@ -143,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
			
		||||
            if (backlink.relationName) {
 | 
			
		||||
                $item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
 | 
			
		||||
            } else {
 | 
			
		||||
                $item.append(...backlink.excerpts);
 | 
			
		||||
                $item.append(...backlink.excerpts ?? []);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.$items.append($item);
 | 
			
		||||
@@ -40,7 +40,7 @@ export default class GeoMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
                const L = (await import("leaflet")).default;
 | 
			
		||||
 | 
			
		||||
                const map = L.map(this.$container[0], {
 | 
			
		||||
 | 
			
		||||
                    worldCopyJump: true
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                this.map = map;
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,9 @@ import type NoteContext from "../components/note_context.js";
 | 
			
		||||
class NoteContextAwareWidget extends BasicWidget {
 | 
			
		||||
    protected noteContext?: NoteContext;
 | 
			
		||||
 | 
			
		||||
    isNoteContext(ntxId: string | null | undefined) {
 | 
			
		||||
    isNoteContext(ntxId: string | string[] | null | undefined) {
 | 
			
		||||
        if (Array.isArray(ntxId)) {
 | 
			
		||||
            return this.noteContext && ntxId.includes(this.noteContext.ntxId);
 | 
			
		||||
            return this.noteContext && this.noteContext.ntxId && ntxId.includes(this.noteContext.ntxId);
 | 
			
		||||
        } else {
 | 
			
		||||
            return this.noteContext && this.noteContext.ntxId === ntxId;
 | 
			
		||||
        }
 | 
			
		||||
@@ -54,7 +54,7 @@ class NoteContextAwareWidget extends BasicWidget {
 | 
			
		||||
     *
 | 
			
		||||
     * @returns true when an active note exists
 | 
			
		||||
     */
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
    isEnabled(): boolean | null | undefined {
 | 
			
		||||
        return !!this.note;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -147,11 +147,14 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
 | 
			
		||||
     */
 | 
			
		||||
    checkFullHeight() {
 | 
			
		||||
        // https://github.com/zadam/trilium/issues/2522
 | 
			
		||||
        this.$widget.toggleClass(
 | 
			
		||||
            "full-height",
 | 
			
		||||
            (!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") ||
 | 
			
		||||
                this.noteContext.viewScope.viewMode === "attachments"
 | 
			
		||||
        );
 | 
			
		||||
        const isBackendNote = this.noteContext?.noteId === "_backendLog";
 | 
			
		||||
        const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
 | 
			
		||||
        const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type);
 | 
			
		||||
        const isFullHeight = (!this.noteContext.hasNoteList() && isFullHeightNoteType && !isSqlNote)
 | 
			
		||||
            || this.noteContext.viewScope.viewMode === "attachments"
 | 
			
		||||
            || isBackendNote;
 | 
			
		||||
 | 
			
		||||
        this.$widget.toggleClass("full-height", isFullHeight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTypeWidget() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import NoteContextAwareWidget from "./note_context_aware_widget.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 = `
 | 
			
		||||
<div class="note-list-widget">
 | 
			
		||||
@@ -19,8 +21,14 @@ const TPL = `
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class NoteListWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private $content!: JQuery<HTMLElement>;
 | 
			
		||||
    private isIntersecting?: boolean;
 | 
			
		||||
    private noteIdRefreshed?: string;
 | 
			
		||||
    private shownNoteId?: string | null;
 | 
			
		||||
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
        return super.isEnabled() && this.noteContext.hasNoteList();
 | 
			
		||||
        return super.isEnabled() && this.noteContext?.hasNoteList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
@@ -50,13 +58,13 @@ export default class NoteListWidget extends NoteContextAwareWidget {
 | 
			
		||||
        // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === 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.renderNoteList(this.note);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async renderNoteList(note) {
 | 
			
		||||
    async renderNoteList(note: FNote) {
 | 
			
		||||
        const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
 | 
			
		||||
        await noteListRenderer.renderList();
 | 
			
		||||
    }
 | 
			
		||||
@@ -67,8 +75,8 @@ export default class NoteListWidget extends NoteContextAwareWidget {
 | 
			
		||||
        await super.refresh();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshNoteListEvent({ noteId }) {
 | 
			
		||||
        if (this.isNote(noteId)) {
 | 
			
		||||
    async refreshNoteListEvent({ noteId }: EventData<"refreshNoteList">) {
 | 
			
		||||
        if (this.isNote(noteId) && 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
 | 
			
		||||
     * it is not intersected (visible) anymore.
 | 
			
		||||
     */
 | 
			
		||||
    noteDetailRefreshedEvent({ ntxId }) {
 | 
			
		||||
    noteDetailRefreshedEvent({ ntxId }: EventData<"noteDetailRefreshed">) {
 | 
			
		||||
        if (!this.isNoteContext(ntxId)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -88,14 +96,14 @@ export default class NoteListWidget extends NoteContextAwareWidget {
 | 
			
		||||
        setTimeout(() => this.checkRenderStatus(), 100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    notesReloadedEvent({ noteIds }) {
 | 
			
		||||
        if (noteIds.includes(this.noteId)) {
 | 
			
		||||
    notesReloadedEvent({ noteIds }: EventData<"notesReloaded">) {
 | 
			
		||||
        if (this.noteId && noteIds.includes(this.noteId)) {
 | 
			
		||||
            this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
        if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
 | 
			
		||||
            this.shownNoteId = null; // force render
 | 
			
		||||
 | 
			
		||||
            this.checkRenderStatus();
 | 
			
		||||
@@ -163,7 +163,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
    private themeStyle!: string;
 | 
			
		||||
    private $container!: JQuery<HTMLElement>;
 | 
			
		||||
    private $styleResolver!: JQuery<HTMLElement>;
 | 
			
		||||
    private graph!: ForceGraph;
 | 
			
		||||
    graph!: ForceGraph;
 | 
			
		||||
    private noteIdToSizeMap!: Record<string, number>;
 | 
			
		||||
    private zoomLevel!: number;
 | 
			
		||||
    private nodes!: Node[];
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,11 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
			
		||||
import protectedSessionHolder from "../services/protected_session_holder.js";
 | 
			
		||||
import server from "../services/server.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 shortcutService from "../services/shortcuts.js";
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import type FNote from "../entities/fnote.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="note-title-widget">
 | 
			
		||||
@@ -33,13 +34,20 @@ const TPL = `
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class NoteTitleWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private $noteTitle!: JQuery<HTMLElement>;
 | 
			
		||||
    private deleteNoteOnEscape: boolean;
 | 
			
		||||
    private spacedUpdate: SpacedUpdate;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.spacedUpdate = new SpacedUpdate(async () => {
 | 
			
		||||
            const title = this.$noteTitle.val();
 | 
			
		||||
 | 
			
		||||
            if (this.note) {
 | 
			
		||||
                protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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", () => {
 | 
			
		||||
            if (this.deleteNoteOnEscape && this.noteContext.isActive()) {
 | 
			
		||||
            if (this.deleteNoteOnEscape && this.noteContext?.isActive() && this.noteContext?.note) {
 | 
			
		||||
                branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        shortcutService.bindElShortcut(this.$noteTitle, "return", () => {
 | 
			
		||||
            this.triggerCommand("focusOnDetail", { ntxId: this.noteContext.ntxId });
 | 
			
		||||
            this.triggerCommand("focusOnDetail", { ntxId: this.noteContext?.ntxId });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshWithNote(note) {
 | 
			
		||||
        const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext.viewScope.viewMode !== "default";
 | 
			
		||||
    async refreshWithNote(note: FNote) {
 | 
			
		||||
        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.setProtectedStatus(note);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @param {FNote} note */
 | 
			
		||||
    setProtectedStatus(note) {
 | 
			
		||||
    setProtectedStatus(note: FNote) {
 | 
			
		||||
        this.$noteTitle.toggleClass("protected", !!note.isProtected);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async beforeNoteSwitchEvent({ noteContext }) {
 | 
			
		||||
    async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) {
 | 
			
		||||
        if (this.isNoteContext(noteContext.ntxId)) {
 | 
			
		||||
            await this.spacedUpdate.updateNowIfNecessary();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async beforeNoteContextRemoveEvent({ ntxIds }) {
 | 
			
		||||
    async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) {
 | 
			
		||||
        if (this.isNoteContext(ntxIds)) {
 | 
			
		||||
            await this.spacedUpdate.updateNowIfNecessary();
 | 
			
		||||
        }
 | 
			
		||||
@@ -112,8 +119,8 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
        if (loadResults.isNoteReloaded(this.noteId)) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        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
 | 
			
		||||
            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: "book", mime: "", title: t("note_types.book"), 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 }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ import { t } from "../../services/i18n.js";
 | 
			
		||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
			
		||||
import server from "../../services/server.js";
 | 
			
		||||
import utils from "../../services/utils.js";
 | 
			
		||||
import type { EventData } from "../../components/app_context.js";
 | 
			
		||||
import type FNote from "../../entities/fnote.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="note-info-widget">
 | 
			
		||||
@@ -61,7 +63,33 @@ const TPL = `
 | 
			
		||||
</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 {
 | 
			
		||||
 | 
			
		||||
    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() {
 | 
			
		||||
        return "noteInfo";
 | 
			
		||||
    }
 | 
			
		||||
@@ -71,7 +99,7 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
        return this.note;
 | 
			
		||||
        return !!this.note;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTitle() {
 | 
			
		||||
@@ -104,10 +132,10 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
			
		||||
            this.$noteSize.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));
 | 
			
		||||
 | 
			
		||||
            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) {
 | 
			
		||||
                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) {
 | 
			
		||||
        const metadata = await server.get(`notes/${this.noteId}/metadata`);
 | 
			
		||||
    async refreshWithNote(note: FNote) {
 | 
			
		||||
        const metadata = await server.get<MetadataResponse>(`notes/${this.noteId}/metadata`);
 | 
			
		||||
 | 
			
		||||
        this.$noteId.text(note.noteId);
 | 
			
		||||
        this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated);
 | 
			
		||||
@@ -137,8 +165,8 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.$noteSizesWrapper.hide();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
        if (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId)) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (this.noteId && (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId))) {
 | 
			
		||||
            this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -33,6 +33,13 @@ const TPL = `
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
@@ -106,7 +113,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    setSmallSize() {
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
@@ -114,9 +121,11 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
 | 
			
		||||
    setFullHeight() {
 | 
			
		||||
        const { top } = this.$widget[0].getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        const height = $(window).height() - top;
 | 
			
		||||
        const width = this.$widget.width();
 | 
			
		||||
        const height = ($(window).height() ?? 0) - top;
 | 
			
		||||
        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 linkService from "../../services/link.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 = `
 | 
			
		||||
<div class="note-paths-widget">
 | 
			
		||||
@@ -37,6 +40,10 @@ const TPL = `
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private $notePathIntro!: JQuery<HTMLElement>;
 | 
			
		||||
    private $notePathList!: JQuery<HTMLElement>;
 | 
			
		||||
 | 
			
		||||
    get name() {
 | 
			
		||||
        return "notePaths";
 | 
			
		||||
    }
 | 
			
		||||
@@ -59,13 +66,12 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        this.$notePathIntro = this.$widget.find(".note-path-intro");
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
        if (this.noteId === "root") {
 | 
			
		||||
        if (!this.note || this.noteId === "root") {
 | 
			
		||||
            this.$notePathList.empty().append(await this.getRenderedPath("root"));
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
@@ -90,7 +96,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
        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 $noteLink = await linkService.createLink(notePath, { title });
 | 
			
		||||
@@ -128,8 +134,9 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
        return $("<li>").append($noteLink);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
        if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || loadResults.isNoteReloaded(this.noteId)) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) ||
 | 
			
		||||
            (this.noteId != null && loadResults.isNoteReloaded(this.noteId))) {
 | 
			
		||||
            this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -3,6 +3,8 @@ import linkService from "../../services/link.js";
 | 
			
		||||
import server from "../../services/server.js";
 | 
			
		||||
import froca from "../../services/froca.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 = `
 | 
			
		||||
<div class="similar-notes-widget">
 | 
			
		||||
@@ -31,7 +33,20 @@ const TPL = `
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// TODO: Deduplicate with server
 | 
			
		||||
interface SimilarNote {
 | 
			
		||||
    score: number;
 | 
			
		||||
    notePath: string[];
 | 
			
		||||
    noteId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private $similarNotesWrapper!: JQuery<HTMLElement>;
 | 
			
		||||
    private title?: string;
 | 
			
		||||
    private rendered?: boolean;
 | 
			
		||||
 | 
			
		||||
    get name() {
 | 
			
		||||
        return "similarNotes";
 | 
			
		||||
    }
 | 
			
		||||
@@ -41,7 +56,7 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
        return super.isEnabled() && this.note.type !== "search" && !this.note.isLabelTruthy("similarNotesWidgetDisabled");
 | 
			
		||||
        return super.isEnabled() && this.note?.type !== "search" && !this.note?.isLabelTruthy("similarNotesWidgetDisabled");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTitle() {
 | 
			
		||||
@@ -59,11 +74,15 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
			
		||||
        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
 | 
			
		||||
        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) {
 | 
			
		||||
            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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (this.note && this.title !== this.note.title) {
 | 
			
		||||
            this.rendered = false;
 | 
			
		||||
 | 
			
		||||
@@ -3,6 +3,7 @@ import BasicWidget from "./basic_widget.js";
 | 
			
		||||
import ws from "../services/ws.js";
 | 
			
		||||
import options from "../services/options.js";
 | 
			
		||||
import syncService from "../services/sync.js";
 | 
			
		||||
import { escapeQuotes } from "../services/utils.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="sync-status-widget launcher-button">
 | 
			
		||||
@@ -41,29 +42,29 @@ const TPL = `
 | 
			
		||||
    <div class="sync-status">
 | 
			
		||||
        <span class="sync-status-icon sync-status-unknown bx bx-time"
 | 
			
		||||
              data-bs-toggle="tooltip"
 | 
			
		||||
              title="${t("sync_status.unknown")}">
 | 
			
		||||
              title="${escapeQuotes(t("sync_status.unknown"))}">
 | 
			
		||||
        </span>
 | 
			
		||||
        <span class="sync-status-icon sync-status-connected-with-changes bx bx-wifi"
 | 
			
		||||
              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>
 | 
			
		||||
        <span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi"
 | 
			
		||||
              data-bs-toggle="tooltip"
 | 
			
		||||
              title="${t("sync_status.connected_no_changes")}">
 | 
			
		||||
              title="${escapeQuotes(t("sync_status.connected_no_changes"))}">
 | 
			
		||||
        </span>
 | 
			
		||||
        <span class="sync-status-icon sync-status-disconnected-with-changes bx bx-wifi-off"
 | 
			
		||||
              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>
 | 
			
		||||
        <span class="sync-status-icon sync-status-disconnected-no-changes bx bx-wifi-off"
 | 
			
		||||
              data-bs-toggle="tooltip"
 | 
			
		||||
              title="${t("sync_status.disconnected_no_changes")}">
 | 
			
		||||
              title="${escapeQuotes(t("sync_status.disconnected_no_changes"))}">
 | 
			
		||||
        </span>
 | 
			
		||||
        <span class="sync-status-icon sync-status-in-progress bx bx-analyse bx-spin"
 | 
			
		||||
              data-bs-toggle="tooltip"
 | 
			
		||||
              title="${t("sync_status.in_progress")}">
 | 
			
		||||
              title="${escapeQuotes(t("sync_status.in_progress"))}">
 | 
			
		||||
        </span>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 10 KiB  | 
@@ -26,6 +26,11 @@
 | 
			
		||||
        border-radius: 2pt !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    span[style] {
 | 
			
		||||
        print-color-adjust: exact;
 | 
			
		||||
        -webkit-print-color-adjust: exact;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Fix visibility of checkbox checkmarks
 | 
			
		||||
       see https://github.com/TriliumNext/Notes/issues/901 */
 | 
			
		||||
    .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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-item > span:not([class]) {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.CodeMirror {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: inherit;
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
 | 
			
		||||
    padding: 0px 10px;
 | 
			
		||||
    letter-spacing: 0.5px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attachment-content-wrapper pre code,
 | 
			
		||||
 
 | 
			
		||||
@@ -1339,7 +1339,7 @@ body .calendar-dropdown-widget .calendar-body a:hover {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1350,7 +1350,7 @@
 | 
			
		||||
    "mermaid-diagram": "Mermaid Diagram",
 | 
			
		||||
    "canvas": "Canvas",
 | 
			
		||||
    "web-view": "Webansicht",
 | 
			
		||||
    "mind-map": "Mind Map (Beta)",
 | 
			
		||||
    "mind-map": "Mind Map",
 | 
			
		||||
    "file": "Datei",
 | 
			
		||||
    "image": "Bild",
 | 
			
		||||
    "launcher": "Launcher",
 | 
			
		||||
 
 | 
			
		||||
@@ -1403,7 +1403,7 @@
 | 
			
		||||
    "mermaid-diagram": "Mermaid Diagram",
 | 
			
		||||
    "canvas": "Canvas",
 | 
			
		||||
    "web-view": "Web View",
 | 
			
		||||
    "mind-map": "Mind Map (Beta)",
 | 
			
		||||
    "mind-map": "Mind Map",
 | 
			
		||||
    "file": "File",
 | 
			
		||||
    "image": "Image",
 | 
			
		||||
    "launcher": "Launcher",
 | 
			
		||||
 
 | 
			
		||||
@@ -1403,7 +1403,7 @@
 | 
			
		||||
    "mermaid-diagram": "Diagrama Mermaid",
 | 
			
		||||
    "canvas": "Lienzo",
 | 
			
		||||
    "web-view": "Vista Web",
 | 
			
		||||
    "mind-map": "Mapa Mental (beta)",
 | 
			
		||||
    "mind-map": "Mapa Mental",
 | 
			
		||||
    "file": "Archivo",
 | 
			
		||||
    "image": "Imagen",
 | 
			
		||||
    "launcher": "Lanzador",
 | 
			
		||||
 
 | 
			
		||||
@@ -1351,7 +1351,7 @@
 | 
			
		||||
    "mermaid-diagram": "Diagramme Mermaid",
 | 
			
		||||
    "canvas": "Canevas",
 | 
			
		||||
    "web-view": "Affichage Web",
 | 
			
		||||
    "mind-map": "Carte mentale (Beta)",
 | 
			
		||||
    "mind-map": "Carte mentale",
 | 
			
		||||
    "file": "Fichier",
 | 
			
		||||
    "image": "Image",
 | 
			
		||||
    "launcher": "Raccourci",
 | 
			
		||||
 
 | 
			
		||||
@@ -1367,7 +1367,7 @@
 | 
			
		||||
    "canvas": "Schiță",
 | 
			
		||||
    "code": "Cod sursă",
 | 
			
		||||
    "mermaid-diagram": "Diagramă Mermaid",
 | 
			
		||||
    "mind-map": "Hartă mentală (beta)",
 | 
			
		||||
    "mind-map": "Hartă mentală",
 | 
			
		||||
    "note-map": "Hartă notițe",
 | 
			
		||||
    "relation-map": "Hartă relații",
 | 
			
		||||
    "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 { Request } from "express";
 | 
			
		||||
 | 
			
		||||
interface Backlink {
 | 
			
		||||
    noteId: string;
 | 
			
		||||
    relationName?: string;
 | 
			
		||||
    excerpts?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildDescendantCountMap(noteIdsToCount: string[]) {
 | 
			
		||||
    if (!Array.isArray(noteIdsToCount)) {
 | 
			
		||||
        throw new Error("noteIdsToCount: type error");
 | 
			
		||||
@@ -325,7 +331,7 @@ function findExcerpts(sourceNote: BNote, referencedNoteId: string) {
 | 
			
		||||
    return excerpts;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getFilteredBacklinks(note: BNote) {
 | 
			
		||||
function getFilteredBacklinks(note: BNote): BAttribute[] {
 | 
			
		||||
    return (
 | 
			
		||||
        note
 | 
			
		||||
            .getTargetRelations()
 | 
			
		||||
@@ -344,7 +350,7 @@ function getBacklinkCount(req: Request) {
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getBacklinks(req: Request) {
 | 
			
		||||
function getBacklinks(req: Request): Backlink[] {
 | 
			
		||||
    const { noteId } = req.params;
 | 
			
		||||
    const note = becca.getNoteOrThrow(noteId);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import fs from "fs";
 | 
			
		||||
import dataDir from "./data_dir.js";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import resourceDir from "./resource_dir.js";
 | 
			
		||||
import { envToBoolean } from "./utils.js";
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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";
 | 
			
		||||
 | 
			
		||||
function handleH1(content: string, title: string) {
 | 
			
		||||
    content = content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => {
 | 
			
		||||
        if (title.trim() === text.trim()) {
 | 
			
		||||
            return ""; // remove whole H1 tag
 | 
			
		||||
        } else {
 | 
			
		||||
            return `<h2>${text}</h2>`;
 | 
			
		||||
    let isFirstH1Handled = false;
 | 
			
		||||
 | 
			
		||||
    return content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => {
 | 
			
		||||
        const convertedContent = `<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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ function getRunAtHours(note: BNote): number[] {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function runNotesWithLabel(runAttrValue: string) {
 | 
			
		||||
    const instanceName = config.General ? config.General.instanceName : null;
 | 
			
		||||
    const instanceName = config.General.instanceName;
 | 
			
		||||
    const currentHours = new Date().getHours();
 | 
			
		||||
    const notes = attributeService.getNotesWithLabel("run", runAttrValue);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
import optionService from "./options.js";
 | 
			
		||||
import type { OptionNames } from "./options_interface.js";
 | 
			
		||||
import config from "./config.js";
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -11,14 +10,14 @@ import config from "./config.js";
 | 
			
		||||
 * to live sync server.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
function get(name: OptionNames) {
 | 
			
		||||
function get(name: keyof typeof config.Sync) {
 | 
			
		||||
  return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    // env variable is the easiest way to guarantee we won't overwrite prod data during development
 | 
			
		||||
    // after copying prod document/data directory
 | 
			
		||||
    getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get("syncServerHost"),
 | 
			
		||||
    getSyncServerHost: () => get("syncServerHost"),
 | 
			
		||||
    isSyncSetup: () => {
 | 
			
		||||
        const syncServerHost = get("syncServerHost");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -295,6 +295,18 @@ export function isString(x: any) {
 | 
			
		||||
    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.
 | 
			
		||||
 * On development builds, this simply refers to the root directory of the application.
 | 
			
		||||
@@ -352,5 +364,6 @@ export default {
 | 
			
		||||
    isString,
 | 
			
		||||
    getResourceDir,
 | 
			
		||||
    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 log from "./log.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 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";
 | 
			
		||||
 | 
			
		||||
if (env.isDev()) {
 | 
			
		||||
@@ -24,7 +24,7 @@ if (env.isDev()) {
 | 
			
		||||
        .on("unlink", debouncedReloadFrontend);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let webSocketServer!: WebSocket.Server;
 | 
			
		||||
let webSocketServer!: WebSocketServer;
 | 
			
		||||
let lastSyncedPush: number | null = null;
 | 
			
		||||
 | 
			
		||||
interface Message {
 | 
			
		||||
@@ -58,8 +58,8 @@ interface Message {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void;
 | 
			
		||||
function init(httpServer: Server, sessionParser: SessionParser) {
 | 
			
		||||
    webSocketServer = new WebSocket.Server({
 | 
			
		||||
function init(httpServer: HttpServer, sessionParser: SessionParser) {
 | 
			
		||||
    webSocketServer = new WebSocketServer({
 | 
			
		||||
        verifyClient: (info, done) => {
 | 
			
		||||
            sessionParser(info.req, {}, () => {
 | 
			
		||||
                const allowed = isElectron() || (info.req as any).session.loggedIn || (config.General && config.General.noAuthentication);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import { fileURLToPath } from "url";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import assetPath from "./src/services/asset_path.js";
 | 
			
		||||
import type { Configuration } from "webpack";
 | 
			
		||||
 | 
			
		||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
 | 
			
		||||
export default {
 | 
			
		||||
const config: Configuration = {
 | 
			
		||||
    mode: "production",
 | 
			
		||||
    entry: {
 | 
			
		||||
        setup: "./src/public/app/setup.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: {
 | 
			
		||||
        publicPath: `${assetPath}/app-dist/`,
 | 
			
		||||
@@ -42,3 +44,5 @@ export default {
 | 
			
		||||
    devtool: "source-map",
 | 
			
		||||
    target: "electron-renderer"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||