Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms

This commit is contained in:
Adorian Doran 2025-01-29 17:26:24 +02:00
commit dee221e18a
73 changed files with 952 additions and 696 deletions

View File

@ -1,7 +1,6 @@
name: Bug Report
description: Report a bug
title: "(Bug report) "
labels: "Type: Bug"
type: "Bug"
body:
- type: textarea
attributes:

View File

@ -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

View File

@ -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

View File

@ -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
.npmrc
View File

@ -1,2 +1 @@
save-prefix = ''
legacy-peer-deps = true

View File

@ -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 \

View File

@ -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

View File

@ -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

View File

@ -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=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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;
}

View File

@ -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> = {

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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")
}));

View File

@ -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;
}

View File

@ -124,6 +124,10 @@ function escapeHtml(str: string) {
return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
}
export function escapeQuotes(value: string) {
return value.replaceAll("\"", "&quot;");
}
function formatSize(size: number) {
size = Math.max(Math.round(size / 1024), 1);

View File

@ -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
});

View File

@ -43,6 +43,7 @@ interface CustomGlobals {
appCssNoteIds: string[];
triliumVersion: string;
TRILIUM_SAFE_MODE: boolean;
platform?: typeof process.platform;
}
type RequireMethod = (moduleName: string) => any;

View File

@ -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>

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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">
@ -8,11 +12,11 @@ const TPL = `
.ribbon-container {
margin-bottom: 5px;
}
.ribbon-top-row {
display: flex;
}
.ribbon-tab-container {
display: flex;
flex-direction: row;
@ -21,10 +25,10 @@ const TPL = `
flex-grow: 1;
flex-flow: row wrap;
}
.ribbon-tab-title {
color: var(--muted-text-color);
border-bottom: 1px solid var(--main-border-color);
border-bottom: 1px solid var(--main-border-color);
min-width: 24px;
flex-basis: 24px;
max-width: max-content;
@ -36,7 +40,7 @@ const TPL = `
position: relative;
top: 3px;
}
.ribbon-tab-title.active {
color: var(--main-text-color);
border-bottom: 3px solid var(--main-text-color);
@ -44,7 +48,7 @@ const TPL = `
overflow: hidden;
text-overflow: ellipsis;
}
.ribbon-tab-title:hover {
cursor: pointer;
}
@ -52,11 +56,11 @@ const TPL = `
.ribbon-tab-title:hover {
color: var(--main-text-color);
}
.ribbon-tab-title:first-of-type {
padding-left: 10px;
}
.ribbon-tab-spacer {
flex-basis: 0;
min-width: 0;
@ -64,41 +68,41 @@ const TPL = `
flex-grow: 1;
border-bottom: 1px solid var(--main-border-color);
}
.ribbon-tab-spacer:last-of-type {
flex-grow: 1;
flex-basis: 0;
min-width: 0;
max-width: 10000px;
}
.ribbon-button-container {
display: flex;
border-bottom: 1px solid var(--main-border-color);
border-bottom: 1px solid var(--main-border-color);
margin-right: 5px;
}
.ribbon-button-container > * {
position: relative;
top: -3px;
margin-left: 10px;
}
.ribbon-body {
display: none;
border-bottom: 1px solid var(--main-border-color);
margin-left: 10px;
margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
}
.ribbon-body.active {
display: block;
}
.ribbon-tab-title-label {
display: none;
}
.ribbon-tab-title.active .ribbon-tab-title-label {
display: inline;
}
@ -108,11 +112,21 @@ const TPL = `
<div class="ribbon-tab-container"></div>
<div class="ribbon-button-container"></div>
</div>
<div class="ribbon-body-container"></div>
</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.
}

View File

@ -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>

View File

@ -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>

View File

@ -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();
}

View File

@ -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">
@ -14,7 +15,7 @@ const TPL = `
.backlinks-widget {
position: relative;
}
.backlinks-ticker {
border-radius: 10px;
border-color: var(--main-border-color);
@ -25,11 +26,11 @@ const TPL = `
justify-content: space-between;
align-items: center;
}
.backlinks-count {
cursor: pointer;
}
.backlinks-items {
z-index: 10;
position: absolute;
@ -42,29 +43,41 @@ const TPL = `
padding: 20px;
overflow-y: auto;
}
.backlink-excerpt {
border-left: 2px solid var(--main-border-color);
padding-left: 10px;
opacity: 80%;
font-size: 90%;
}
.backlink-excerpt .backlink-link { /* the actual backlink */
font-weight: bold;
background-color: yellow;
}
</style>
<div class="backlinks-ticker">
<span class="backlinks-count"></span>
</div>
</div>
<div class="backlinks-items" style="display: none;"></div>
</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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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() {

View File

@ -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">
@ -8,19 +10,25 @@ const TPL = `
min-height: 0;
overflow: auto;
}
.note-list-widget .note-list {
padding: 10px;
}
</style>
<div class="note-list-widget-content">
</div>
</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();

View File

@ -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[];

View File

@ -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);
}

View File

@ -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 }
];

View File

@ -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">
@ -10,18 +12,18 @@ const TPL = `
.note-info-widget {
padding: 12px;
}
.note-info-widget-table {
max-width: 100%;
max-width: 100%;
display: block;
overflow-x: auto;
white-space: nowrap;
}
}
.note-info-widget-table td, .note-info-widget-table th {
padding: 5px;
}
.note-info-mime {
max-width: 13em;
overflow: hidden;
@ -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();
}
}

View File

@ -8,18 +8,18 @@ const TPL = `
.note-map-ribbon-widget {
position: relative;
}
.note-map-ribbon-widget .note-map-container {
height: 300px;
}
.open-full-button, .collapse-button {
position: absolute;
right: 5px;
bottom: 5px;
z-index: 1000;
}
.style-resolver {
color: var(--muted-text-color);
display: none;
@ -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);
}
}

View File

@ -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();
}
}

View File

@ -3,10 +3,12 @@ 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">
<style>
<style>
.similar-notes-wrapper {
max-height: 200px;
overflow: auto;
@ -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;

View File

@ -3,35 +3,36 @@ 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">
<style>
.sync-status-widget {
}
.sync-status {
box-sizing: border-box;
}
.sync-status .sync-status-icon {
display: inline-block;
position: relative;
top: -5px;
font-size: 110%;
}
.sync-status .sync-status-sub-icon {
font-size: 40%;
position: absolute;
font-size: 40%;
position: absolute;
left: 0;
top: 16px;
}
.sync-status .sync-status-icon span {
border: none !important;
}
.sync-status-icon:not(.sync-status-in-progress):hover {
background-color: var(--hover-item-background-color);
cursor: pointer;
@ -39,31 +40,31 @@ const TPL = `
</style>
<div class="sync-status">
<span class="sync-status-icon sync-status-unknown bx bx-time"
data-bs-toggle="tooltip"
title="${t("sync_status.unknown")}">
<span class="sync-status-icon sync-status-unknown bx bx-time"
data-bs-toggle="tooltip"
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")}">
data-bs-toggle="tooltip"
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")}">
<span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi"
data-bs-toggle="tooltip"
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")}">
data-bs-toggle="tooltip"
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"
<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"
<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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -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 {

View File

@ -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;

View File

@ -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,

View File

@ -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;
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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ță",

View File

@ -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);

View File

@ -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;

View 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);
});
});
});

View File

@ -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 {

View File

@ -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);

View File

@ -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");

View File

@ -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
};

View File

@ -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);

View File

@ -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;