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; | ||||
 Adorian Doran
						Adorian Doran