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
|
||||
|
24
.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
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
|
20
.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
|
||||
runs-on: ubuntu-latest
|
||||
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();
|
||||
|
||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
||||
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) {
|
||||
return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
|
||||
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;
|