Compare commits

..

2 Commits

Author SHA1 Message Date
Elian Doran 5dc546605b Prepare for 0.91.3-beta
/ Check Docker build (Dockerfile) (push) Failing after 44s
/ Check Docker build (Dockerfile.alpine) (push) Failing after 33s
/ Build Docker images (Dockerfile, linux/arm/v7) (push) Has been skipped
/ Build Docker images (Dockerfile, linux/arm64) (push) Has been skipped
/ Build Docker images (Dockerfile.alpine, linux/amd64) (push) Has been skipped
/ Merge manifest lists (push) Has been skipped
Release / Make Electron (arm64, map[extension:deb image:ubuntu-latest name:linux]) (push) Failing after 38s
Release / Make Electron (x64, map[extension:deb image:ubuntu-latest name:linux]) (push) Failing after 41s
Release / Build Linux Server x86_64 (push) Failing after 34s
Release / Make Electron (arm64, map[extension:dmg image:macos-latest name:macos]) (push) Has been cancelled
Release / Make Electron (arm64, map[extension:exe image:windows-latest name:windows]) (push) Has been cancelled
Release / Make Electron (x64, map[extension:dmg image:macos-latest name:macos]) (push) Has been cancelled
Release / Make Electron (x64, map[extension:exe image:windows-latest name:windows]) (push) Has been cancelled
2025-01-16 23:05:53 +02:00
Elian Doran 62cda1bd24 Merge pull request #960 from pano9000/fix_csrf-csrf_existing_cookie
fix(csrf): fix handling of existing _csrf cookies
2025-01-16 21:54:11 +02:00
160 changed files with 3006 additions and 4747 deletions
+1 -2
View File
@@ -1,3 +1,2 @@
package-lock.json linguist-generated=true
**/package-lock.json linguist-generated=true
libraries/** linguist-vendored
**/package-lock.json linguist-generated=true
+8 -18
View File
@@ -72,18 +72,9 @@ jobs:
with:
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}}
path: upload/*.${{ matrix.os.extension }}
build_linux_server:
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 }}
build_linux_server-x64:
name: Build Linux Server x86_64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up node & dependencies
@@ -93,18 +84,17 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Linux server build
env:
MATRIX_ARCH: ${{ matrix.arch }}
- name: Run Linux server build (x86_64)
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-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
- uses: actions/upload-artifact@v4
with:
name: TriliumNextNotes linux server ${{ matrix.arch }}
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
name: TriliumNextNotes linux server x64
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
+8 -16
View File
@@ -92,16 +92,7 @@ jobs:
asset_content_type: application/zip # required by GitHub API
nightly-server:
name: Deploy server nightly
strategy:
fail-fast: false
matrix:
arch: [x64, arm64]
include:
- arch: x64
runs-on: ubuntu-latest
- arch: arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.runs-on }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up node & dependencies
@@ -111,21 +102,22 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Linux server build
env:
MATRIX_ARCH: ${{ matrix.arch }}
- name: Run Linux server build (x86_64)
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-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
- uses: actions/upload-artifact@v4
with:
name: TriliumNextNotes linux server ${{ matrix.arch }}
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
name: TriliumNextNotes linux server x64
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
overwrite: true
- name: Deploy release
uses: WebFreak001/deploy-nightly@v3.2.0
+5 -15
View File
@@ -66,17 +66,8 @@ jobs:
fail_on_unmatched_files: true
files: upload/*.*
build_linux_server-x64:
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 }}
name: Build Linux Server x86_64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up node & dependencies
@@ -86,17 +77,16 @@ jobs:
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run Linux server build
env:
MATRIX_ARCH: ${{ matrix.arch }}
- name: Run Linux server build (x86_64)
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-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-server-linux-x64.tar.xz"
- name: Publish release
uses: softprops/action-gh-release@v2
with:
-1
View File
@@ -2,7 +2,6 @@
node_modules/
dist/
build/
coverage/
src/public/app-dist/
npm-debug.log
yarn-error.log
+1
View File
@@ -1 +1,2 @@
save-prefix = ''
legacy-peer-deps = true
+3 -2
View File
@@ -1,5 +1,5 @@
# Build stage
FROM node:22.13.1-bullseye-slim AS builder
FROM node:22.13.0-bullseye-slim AS builder
# Configure build dependencies in a single layer
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -28,6 +28,7 @@ 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 +37,7 @@ RUN cp -R build/src/* src/. && \
rm -r build
# Runtime stage
FROM node:22.13.1-bullseye-slim
FROM node:22.13.0-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
+3 -2
View File
@@ -1,5 +1,5 @@
# Build stage
FROM node:22.13.1-alpine AS builder
FROM node:22.13.0-alpine AS builder
# Configure build dependencies
RUN apk add --no-cache --virtual .build-dependencies \
@@ -27,6 +27,7 @@ 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 && \
@@ -35,7 +36,7 @@ RUN cp -R build/src/* src/. && \
rm -r build
# Runtime stage
FROM node:22.13.1-alpine
FROM node:22.13.0-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow
+7 -43
View File
@@ -2,57 +2,21 @@
set -e # Fail on any command error
# Debug output
echo "Matrix Arch: $MATRIX_ARCH"
# Detect architecture from matrix input, fallback to system architecture
if [ -n "$MATRIX_ARCH" ]; then
ARCH=$MATRIX_ARCH
else
ARCH=$(uname -m)
# Convert system architecture to our naming convention
case $ARCH in
x86_64) ARCH="x64" ;;
aarch64) ARCH="arm64" ;;
esac
fi
# Debug output
echo "Selected Arch: $ARCH"
# Set Node.js version and architecture-specific filename
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=20.15.1
NODE_ARCH=$ARCH
# Debug output
echo "Node arch: $NODE_ARCH"
# Special case for x64 in Node.js downloads
if [ "$NODE_ARCH" = "x64" ]; then
NODE_FILENAME="x64"
elif [ "$NODE_ARCH" = "arm64" ]; then
NODE_FILENAME="arm64"
fi
# Debug output
echo "Node filename: $NODE_FILENAME"
PKG_DIR=dist/trilium-linux-${ARCH}-server
echo "Package directory: $PKG_DIR"
if [ "$1" != "DONTCOPY" ]
then
# Need to modify copy-trilium.sh to accept the target directory
./bin/copy-trilium.sh "$PKG_DIR"
./bin/copy-trilium.sh $PKG_DIR
fi
cd dist
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
tar xfJ node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
rm node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz
tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz
rm node-v${NODE_VERSION}-linux-x64.tar.xz
cd ..
mv dist/node-v${NODE_VERSION}-linux-${NODE_FILENAME} $PKG_DIR/node
mv dist/node-v${NODE_VERSION}-linux-x64 $PKG_DIR/node
rm -r $PKG_DIR/node/lib/node_modules/npm
rm -r $PKG_DIR/node/include/node
@@ -73,4 +37,4 @@ VERSION=`jq -r ".version" package.json`
cd dist
tar cJf trilium-linux-${ARCH}-server-${VERSION}.tar.xz trilium-linux-${ARCH}-server
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server
+2 -2
View File
@@ -76,6 +76,7 @@ const copy = async () => {
"node_modules/@excalidraw/excalidraw/dist/",
"node_modules/katex/dist/",
"node_modules/dayjs/",
"node_modules/force-graph/dist/",
"node_modules/boxicons/css/",
"node_modules/boxicons/fonts/",
"node_modules/mermaid/dist/",
@@ -100,8 +101,7 @@ const copy = async () => {
"node_modules/codemirror/keymap/",
"node_modules/mind-elixir/dist/",
"node_modules/@highlightjs/cdn-assets/languages",
"node_modules/@highlightjs/cdn-assets/styles",
"node_modules/leaflet/dist"
"node_modules/@highlightjs/cdn-assets/styles"
];
for (const folder of nodeModulesFolder) {
+1
View File
@@ -68,6 +68,7 @@ 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
-5
View File
@@ -27,8 +27,3 @@ 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=
BIN
View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

+4 -1
View File
@@ -1,5 +1,8 @@
import http from "http";
import config from "./src/services/config.js";
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"));
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: 16 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

+1 -1
View File
@@ -12,7 +12,7 @@ function getDataKey(password: any) {
const encryptedDataKey = getOption("encryptedDataKey");
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey);
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
return decryptedDataKey;
} catch (e: any) {
+1 -3
View File
@@ -16,7 +16,7 @@ function decryptString(dataKey: any, cipherText: any) {
return str;
}
function decrypt(key: any, cipherText: any) {
function decrypt(key: any, cipherText: any, ivLength = 13) {
if (cipherText === null) {
return null;
}
@@ -27,8 +27,6 @@ function decrypt(key: any, cipherText: any) {
try {
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
const iv = cipherTextBufferWithIv.slice(0, ivLength);
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
+6 -6
View File
@@ -464,9 +464,9 @@
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "11.8.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
"integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz",
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==",
"hasInstallScript": true,
"dependencies": {
"bindings": "^1.5.0",
@@ -1516,9 +1516,9 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"better-sqlite3": {
"version": "11.8.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
"integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz",
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==",
"requires": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
-27
View File
@@ -1,27 +0,0 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test("Help popup", async ({ page, context }) => {
page.setDefaultTimeout(15_000);
const app = new App(page, context);
await app.goto();
const popupPromise = page.waitForEvent("popup");
await app.currentNoteSplit.press("F1");
await page.getByRole("link", { name: "online↗" }).click();
const popup = await popupPromise;
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/");
});
test("Complete help in search", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.launcherBar.locator(".bx-search").first().click();
await app.currentNoteSplit.locator(".search-settings .bx-help-circle").click();
const popupPromise = page.waitForEvent("popup");
await page.getByRole("link", { name: "complete help on search syntax" }).click();
const popup = await popupPromise;
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
});
-67
View File
@@ -1,67 +0,0 @@
import { test, expect, Page, BrowserContext } from "@playwright/test";
import App from "../support/app";
test("renders ELK flowchart", async ({ page, context }) => {
await testAriaSnapshot({
page, context,
noteTitle: "Flowchart ELK on",
snapshot: `
- document:
- paragraph: A
- paragraph: B
- paragraph: C
- paragraph: Guarantee
- paragraph: User attributes
- paragraph: Master data
- paragraph: Exchange Rate
- paragraph: Profit Centers
- paragraph: Vendor Partners
- paragraph: Work Situation
- paragraph: Customer
- paragraph: Profit Centers
- paragraph: Guarantee
- text: Interfaces for B
`
})
});
test("renders standard flowchart", async ({ page, context }) => {
await testAriaSnapshot({
page, context,
noteTitle: "Flowchart ELK off",
snapshot: `
- document:
- paragraph: Guarantee
- paragraph: User attributes
- paragraph: Master data
- paragraph: Exchange Rate
- paragraph: Profit Centers
- paragraph: Vendor Partners
- paragraph: Work Situation
- paragraph: Customer
- paragraph: Profit Centers
- paragraph: Guarantee
- paragraph: A
- paragraph: B
- paragraph: C
- text: Interfaces for B
`
})
});
interface AriaTestOpts {
page: Page;
context: BrowserContext;
noteTitle: string;
snapshot: string;
}
async function testAriaSnapshot({ page, context, noteTitle, snapshot }: AriaTestOpts) {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab(noteTitle);
const svgData = app.currentNoteSplit.locator(".mermaid-render svg");
await expect(svgData).toBeVisible();
await expect(svgData).toMatchAriaSnapshot(snapshot);
}
@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect, Page } from "@playwright/test";
import App from "../support/app";
test("displays simple map", async ({ page, context }) => {
-9
View File
@@ -1,9 +0,0 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
test("renders global map", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.launcherBar.locator(".launcher-button.bx-map-alt").click();
await expect(app.currentNoteSplit.locator(".force-graph-container canvas")).toBeVisible();
});
-17
View File
@@ -49,20 +49,3 @@ test("Highlights list is displayed", async ({ page, context }) => {
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
}
});
test("Displays math popup", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Empty text");
const noteContent = app.currentNoteSplit.locator(".note-detail-editable-text-editor")
await noteContent.fill("Hello world");
await noteContent.press("ControlOrMeta+M");
const mathForm = page.locator(".ck-math-form");
await expect(mathForm).toBeVisible();
await mathForm.locator(".ck-input").first().fill("e=mc^2");
const preview = page.locator('[id^="math-preview"]');
await expect(preview).toMatchAriaSnapshot("- math: e = m c 2");
});
-18
View File
@@ -1,18 +0,0 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test("Goes to share root", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "/share" });
const noteTitle = "Shared notes";
await expect(page).toHaveTitle(noteTitle);
await expect(page.locator("h1")).toHaveText(noteTitle);
});
test("Goes to parent share root page", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "/share/bKMn5EFv9KS2" });
await expect(page.locator("h1")).toHaveText("Child note");
await page.locator("#parentLink a").click();
await page.waitForURL("/share/");
});
+6 -15
View File
@@ -2,7 +2,6 @@ import { expect, Locator, Page } from "@playwright/test";
import type { BrowserContext } from "@playwright/test";
interface GotoOpts {
url?: string;
isMobile?: boolean;
}
@@ -14,7 +13,6 @@ export default class App {
readonly tabBar: Locator;
readonly noteTree: Locator;
readonly launcherBar: Locator;
readonly currentNoteSplit: Locator;
readonly sidebar: Locator;
@@ -24,32 +22,25 @@ export default class App {
this.tabBar = page.locator(".tab-row-widget-container");
this.noteTree = page.locator(".tree-wrapper");
this.launcherBar = page.locator("#launcher-container");
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)")
this.sidebar = page.locator("#right-pane");
}
async goto({ url, isMobile }: GotoOpts = {}) {
async goto(opts: GotoOpts = {}) {
await this.context.addCookies([
{
url: BASE_URL,
name: "trilium-device",
value: isMobile ? "mobile" : "desktop"
value: opts.isMobile ? "mobile" : "desktop"
}
]);
if (!url) {
url = "/";
}
await this.page.goto(url, { waitUntil: "networkidle" });
await this.page.goto("/", { waitUntil: "networkidle" });
// Wait for the page to load.
if (url === "/") {
await expect(this.page.locator(".tree"))
.toContainText("Trilium Integration Test");
await this.closeAllTabs();
}
await expect(this.page.locator(".tree"))
.toContainText("Trilium Integration Test");
await this.closeAllTabs();
}
async goToNoteInNewTab(noteTitle: string) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
import test, { expect } from "@playwright/test";
test("Help popup", async ({ page }) => {
await page.goto("http://localhost:8082");
await page.getByText("Trilium Integration Test DB").click();
await page.locator("body").press("F1");
await page.getByRole("link", { name: "online↗" }).click();
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/");
});
test("Complete help in search", async ({ page }) => {
await page.goto("http://localhost:8082");
// Clear all tabs
await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" });
await page.getByText("Close all tabs").click();
await page.locator("#launcher-container").getByRole("button", { name: "" }).first().click();
await page.getByRole("cell", { name: " " }).locator("span").first().click();
await page.getByRole("button", { name: "complete help on search syntax" }).click();
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
});
+17
View File
@@ -0,0 +1,17 @@
import { test, expect } from "@playwright/test";
const ROOT_URL = "http://localhost:8080";
const LOGIN_PASSWORD = "eliandoran";
test("Can insert equations", async ({ page }) => {
await page.setDefaultTimeout(60_000);
await page.setDefaultNavigationTimeout(60_000);
// Create a new note
// await page.locator("button.button-widget.bx-file-blank")
// .click();
const activeNote = page.locator(".component.note-split:visible");
const noteContent = activeNote.locator(".note-detail-editable-text-editor");
await noteContent.press("Ctrl+M");
});
+5
View File
@@ -0,0 +1,5 @@
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
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mermaid-elk",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mermaid-elk",
"version": "1.0.0",
"license": "ISC"
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mermaid-elk",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "cross-env node --import ../../loader-register.js ../../node_modules/webpack/bin/webpack.js -c webpack.config.cjs"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {}
}
+19
View File
@@ -0,0 +1,19 @@
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: "../../node_modules/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs",
output: {
library: "MERMAID_ELK",
filename: "elk.min.js",
path: path.resolve(__dirname),
libraryTarget: "umd",
libraryExport: "default"
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
}
+10
View File
@@ -0,0 +1,10 @@
// Used to register the loader with Node.js
// This is used to avoid the warning message when using the loader
// Can be removed if this PR is merged:
// https://github.com/TypeStrong/ts-node/pull/2073
// Then probably can change webpack comand to
// "webpack": "cross-env NODE_OPTIONS=--import=ts-node/esm webpack -c webpack.config.ts",
import { register } from "node:module";
import { pathToFileURL } from "node:url";
register("ts-node/esm", pathToFileURL("./"));
+1487 -1846
View File
File diff suppressed because it is too large Load Diff
+27 -30
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "TriliumNext Notes",
"description": "Build your personal knowledge base with TriliumNext Notes",
"version": "0.91.4-beta",
"version": "0.91.3-beta",
"license": "AGPL-3.0-only",
"main": "./dist/electron-main.js",
"author": {
@@ -28,17 +28,18 @@
"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-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --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 .\"",
"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 .\"",
"qstart-electron": "npm run switch-electron && npm run start-electron",
"switch-server": "rimraf ./node_modules/better-sqlite3 && npm install",
"switch-electron": "electron-rebuild",
"switch-electron": "npx electron-rebuild",
"build-backend-docs": "rimraf ./docs/backend_api && typedoc ./docs/backend_api src/becca/entities/*.ts src/services/backend_script_api.ts src/services/sql.ts",
"build-frontend-docs": "rimraf ./docs/frontend_api && jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/basic_widget.js src/public/app/widgets/note_context_aware_widget.js src/public/app/widgets/right_panel_widget.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
"webpack": "tsx node_modules/webpack/bin/webpack.js -c webpack.config.ts",
"webpack": "cross-env node --import ./loader-register.js node_modules/webpack/bin/webpack.js -c webpack.config.ts",
"test-playwright": "playwright test",
"test": "cross-env TRILIUM_DATA_DIR=./data-test vitest",
"test-coverage": "cross-env TRILIUM_DATA_DIR=./data-test vitest --coverage",
"test-jasmine": "cross-env TRILIUM_DATA_DIR=./data-test tsx ./node_modules/jasmine/bin/jasmine.js",
"test-es6": "tsx -r esm spec-es6/attribute_parser.spec.ts",
"test": "npm run test-jasmine && npm run test-es6",
"start-electron-forge": "npm run prepare-dist && electron-forge start",
"make-electron": "npm run webpack && npm run prepare-dist && electron-forge make",
"package-electron": "electron-forge package",
@@ -48,7 +49,7 @@
"integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000",
"ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts",
"prettier-check": "prettier . --check",
"prettier-fix": "prettier . --write"
@@ -59,15 +60,13 @@
"@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.11.1",
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.4",
"@mind-elixir/node-menu": "1.0.3",
"@triliumnext/express-partial-content": "1.0.1",
"@types/leaflet": "1.9.16",
"@types/react-dom": "18.3.5",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
"axios": "1.7.9",
"better-sqlite3": "11.8.1",
"better-sqlite3": "11.7.2",
"bootstrap": "5.3.3",
"boxicons": "2.1.4",
"cheerio": "1.0.0",
@@ -90,16 +89,16 @@
"express": "4.21.2",
"express-rate-limit": "7.5.0",
"express-session": "1.18.1",
"force-graph": "1.49.0",
"fs-extra": "11.3.0",
"force-graph": "1.47.2",
"fs-extra": "11.2.0",
"helmet": "8.0.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "24.2.2",
"i18next": "24.2.1",
"i18next-fs-backend": "2.6.0",
"i18next-http-backend": "3.0.2",
"i18next-http-backend": "3.0.1",
"image-type": "5.2.0",
"ini": "5.0.0",
"is-animated": "2.0.2",
@@ -111,9 +110,8 @@
"jquery.fancytree": "2.38.4",
"jsdom": "26.0.0",
"jsplumb": "2.15.6",
"katex": "0.16.21",
"katex": "0.16.19",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"mark.js": "8.11.1",
"marked": "15.0.6",
"mermaid": "11.4.1",
@@ -148,14 +146,14 @@
"yauzl": "3.2.0"
},
"devDependencies": {
"@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-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/rebuild": "3.7.1",
"@playwright/test": "1.50.0",
"@playwright/test": "1.49.1",
"@types/archiver": "6.0.3",
"@types/better-sqlite3": "7.6.12",
"@types/bootstrap": "5.2.10",
@@ -177,8 +175,7 @@
"@types/jsdom": "21.1.7",
"@types/mime-types": "2.1.4",
"@types/multer": "1.4.12",
"@types/node": "22.10.10",
"@types/react": "18.3.18",
"@types/node": "22.10.6",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.13.0",
"@types/sax": "1.2.7",
@@ -189,13 +186,13 @@
"@types/stream-throttle": "0.1.4",
"@types/tmp": "0.2.6",
"@types/turndown": "5.0.5",
"@types/ws": "8.5.14",
"@types/ws": "8.5.13",
"@types/xml2js": "0.4.14",
"@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.0.4",
"cross-env": "7.0.3",
"electron": "34.0.1",
"electron": "33.3.1",
"esm": "3.2.25",
"iconsur": "1.7.0",
"jasmine": "5.5.0",
"jsdoc": "4.0.4",
"lorem-ipsum": "2.0.8",
@@ -203,11 +200,11 @@
"prettier": "3.4.2",
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"ts-node": "10.9.2",
"tslib": "2.8.1",
"tsx": "4.19.2",
"typedoc": "0.27.6",
"typescript": "5.7.3",
"vitest": "3.0.4",
"webpack": "5.97.1",
"webpack-cli": "6.0.1",
"webpack-dev-middleware": "7.4.2"
+4 -4
View File
@@ -41,10 +41,10 @@ export default defineConfig({
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// {
// name: 'webkit',
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import attributeParser from "./attribute_parser.js";
import * as attributeParser from "../src/public/app/services/attribute_parser.js";
import { describe, it, expect, execute } from "./mini_test.js";
describe("Lexing", () => {
it("simple label", () => {
@@ -40,10 +40,8 @@ describe("Lexing", () => {
});
});
describe.todo("Parser", () => {
/* #TODO
describe("Parser", () => {
it("simple label", () => {
const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));
expect(attrs.length).toEqual(1);
@@ -87,7 +85,6 @@ describe.todo("Parser", () => {
expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual("NFi2gL4xtPxM");
});
*/
});
describe("error cases", () => {
@@ -99,3 +96,5 @@ describe("error cases", () => {
expect(() => attributeParser.lexAndParse("#")).toThrow(`Attribute name is empty, please fill the name.`);
});
});
execute();
+138
View File
@@ -0,0 +1,138 @@
import { describe, it, execute, expect } from "./mini_test.ts";
import { getPlatformAppDataDir, getDataDirs} from "../src/services/data_dir.ts"
describe("data_dir.ts unit tests", () => {
describe("#getPlatformAppDataDir()", () => {
type TestCaseGetPlatformAppDataDir = [
description: string,
fnValue: Parameters<typeof getPlatformAppDataDir>,
expectedValueFn: (val: ReturnType<typeof getPlatformAppDataDir>) => boolean
]
const testCases: TestCaseGetPlatformAppDataDir[] = [
[
"w/ unsupported OS it should return 'null'",
["aix", undefined],
(val) => val === null
],
[
"w/ win32 and no APPDATA set it should return 'null'",
["win32", undefined],
(val) => val === null
],
[
"w/ win32 and set APPDATA it should return set 'APPDATA'",
["win32", "AppData"],
(val) => val === "AppData"
],
[
"w/ linux it should return '/.local/share'",
["linux", undefined],
(val) => val !== null && val.endsWith("/.local/share")
],
[
"w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share",
["linux", "FakeAppData"],
(val) => val !== null && val.endsWith("/.local/share")
],
[
"w/ darwin it should return /Library/Application Support",
["darwin", undefined],
(val) => val !== null && val.endsWith("/Library/Application Support")
],
];
testCases.forEach(testCase => {
const [testDescription, value, isExpected] = testCase;
return it(testDescription, () => {
const actual = getPlatformAppDataDir(...value);
const result = isExpected(actual);
expect(result).toBeTruthy()
})
})
})
describe("#getTriliumDataDir", () => {
// TODO
})
describe("#getDataDirs()", () => {
const envKeys: Omit<keyof ReturnType<typeof getDataDirs>, "TRILIUM_DATA_DIR">[] = [
"DOCUMENT_PATH",
"BACKUP_DIR",
"LOG_DIR",
"ANONYMIZED_DB_DIR",
"CONFIG_INI_PATH",
];
const setMockedEnv = (prefix: string | null) => {
envKeys.forEach(key => {
if (prefix) {
process.env[`TRILIUM_${key}`] = `${prefix}_${key}`
} else {
delete process.env[`TRILIUM_${key}`]
}
})
};
it("w/ process.env values present, it should return an object using values from process.env", () => {
// set mocked values
const mockValuePrefix = "MOCK";
setMockedEnv(mockValuePrefix);
// get result
const result = getDataDirs(`${mockValuePrefix}_TRILIUM_DATA_DIR`);
for (const key in result) {
expect(result[key]).toEqual(`${mockValuePrefix}_${key}`)
}
})
it("w/ NO process.env values present, it should return an object using supplied TRILIUM_DATA_DIR as base", () => {
// make sure values are undefined
setMockedEnv(null);
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
const result = getDataDirs(mockDataDir);
for (const key in result) {
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
}
})
it("should ignore attempts to change a property on the returned object", () => {
// make sure values are undefined
setMockedEnv(null);
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
const result = getDataDirs(mockDataDir);
//@ts-expect-error - attempt to change value of readonly property
result.BACKUP_DIR = "attempt to change";
for (const key in result) {
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
}
})
})
});
execute()
+79
View File
@@ -0,0 +1,79 @@
export function describe(name: string, cb: () => any) {
console.log(`Running ${name}`);
cb();
}
export async function it(name: string, cb: () => any) {
console.log(` Running ${name}`);
cb();
}
let errorCount = 0;
export function expect(val: any) {
return {
toEqual: (comparedVal: any) => {
const jsonVal = JSON.stringify(val);
const comparedJsonVal = JSON.stringify(comparedVal);
if (jsonVal !== comparedJsonVal) {
console.trace("toEqual check failed.");
console.error(`expected: ${comparedJsonVal}`);
console.error(`got: ${jsonVal}`);
errorCount++;
}
},
toBeTruthy: () => {
if (!val) {
console.trace("toBeTruthy failed.");
console.error(`expected: truthy value`);
console.error(`got: ${val}`);
errorCount++;
}
},
toBeFalsy: () => {
if (!!val) {
console.trace("toBeFalsy failed.");
console.error(`expected: null, false, undefined, 0 or empty string`);
console.error(`got: ${val}`);
errorCount++;
}
},
toThrow: (errorMessage: any) => {
try {
val();
} catch (e: any) {
if (e.message !== errorMessage) {
console.trace("toThrow caught exception, but messages differ");
console.error(`expected: ${errorMessage}`);
console.error(`got: ${e.message}`);
console.error(`${e.stack}`);
errorCount++;
}
return;
}
console.trace("toThrow did not catch any exception.");
console.error(`expected: ${errorMessage}`);
console.error(`got: [none]`);
errorCount++;
}
};
}
export function execute() {
console.log("");
if (errorCount) {
console.log(`!!!${errorCount} tests failed!!!`);
} else {
console.log("All tests passed!");
}
}
@@ -1,5 +1,5 @@
import { expect, describe, it } from "vitest";
import sanitizeAttributeName from "./sanitize_attribute_name.js";
import sanitizeAttributeName from "../src/services/sanitize_attribute_name";
import { describe, it, execute, expect } from "./mini_test";
// fn value, expected value
const testCases: [fnValue: string, expectedValue: string][] = [
@@ -31,7 +31,9 @@ describe("sanitizeAttributeName unit tests", () => {
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
const [value, expected] = testCase;
const actual = sanitizeAttributeName(value);
expect(actual).toStrictEqual(expected);
expect(actual).toEqual(expected);
});
});
});
});
execute();
@@ -1,5 +1,5 @@
import { expect, describe, it } from "vitest";
import { formatDownloadTitle } from "./utils.js";
import { formatDownloadTitle } from "../../src/services/utils.ts";
import { describe, it, execute, expect } from "../mini_test.ts";
const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
// empty fileName tests
@@ -55,7 +55,9 @@ describe("utils/formatDownloadTitle unit tests", () => {
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
const [value, expected] = testCase;
const actual = formatDownloadTitle(...value);
expect(actual).toStrictEqual(expected);
expect(actual).toEqual(expected);
});
});
});
});
execute();
+3
View File
@@ -0,0 +1,3 @@
describe("Notes", () => {
it("zzz", () => {});
});
@@ -1,5 +1,4 @@
import { describe, it, expect } from "vitest";
import lex from "./lex.js";
import lex from "../../src/services/search/services/lex.js";
describe("Lexer fulltext", () => {
it("simple lexing", () => {
@@ -1,6 +1,5 @@
import { describe, it, expect } from "vitest";
import handleParens from "./handle_parens.js";
import type { TokenStructure } from "./types.js";
import handleParens from "../../src/services/search/services/handle_parens.js";
import type { TokenStructure } from "../../src/services/search/services/types.js";
describe("Parens handler", () => {
it("handles parens", () => {
@@ -1,16 +1,15 @@
import { describe, it, expect } from "vitest";
import AndExp from "../../search/expressions/and.js";
import AttributeExistsExp from "../../search/expressions/attribute_exists.js";
import type Expression from "../../search/expressions/expression.js";
import LabelComparisonExp from "../../search/expressions/label_comparison.js";
import NotExp from "../../search/expressions/not.js";
import NoteContentFulltextExp from "../../search/expressions/note_content_fulltext.js";
import NoteFlatTextExp from "../../search/expressions/note_flat_text.js";
import OrExp from "../../search/expressions/or.js";
import OrderByAndLimitExp from "../../search/expressions/order_by_and_limit.js";
import PropertyComparisonExp from "../../search/expressions/property_comparison.js";
import SearchContext from "../../search/search_context.js";
import { default as parseInternal, type ParseOpts } from "./parse.js";
import AndExp from "../../src/services/search/expressions/and.js";
import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js";
import type Expression from "../../src/services/search/expressions/expression.js";
import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js";
import NotExp from "../../src/services/search/expressions/not.js";
import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js";
import NoteFlatTextExp from "../../src/services/search/expressions/note_flat_text.js";
import OrExp from "../../src/services/search/expressions/or.js";
import OrderByAndLimitExp from "../../src/services/search/expressions/order_by_and_limit.js";
import PropertyComparisonExp from "../../src/services/search/expressions/property_comparison.js";
import SearchContext from "../../src/services/search/search_context.js";
import { default as parseInternal, type ParseOpts } from "../../src/services/search/services/parse.js";
describe("Parser", () => {
it("fulltext parser without content", () => {
@@ -1,11 +1,10 @@
import { describe, it, expect, beforeEach, } from "vitest";
import searchService from "./search.js";
import BNote from "../../../becca/entities/bnote.js";
import BBranch from "../../../becca/entities/bbranch.js";
import SearchContext from "../search_context.js";
import dateUtils from "../../date_utils.js";
import becca from "../../../becca/becca.js";
import becca_mocking from "../../../../spec/support/becca_mocking.js";
import searchService from "../../src/services/search/services/search.js";
import BNote from "../../src/becca/entities/bnote.js";
import BBranch from "../../src/becca/entities/bbranch.js";
import SearchContext from "../../src/services/search/search_context.js";
import dateUtils from "../../src/services/date_utils.js";
import becca from "../../src/becca/becca.js";
import becca_mocking from "./becca_mocking.js";
describe("Search", () => {
let rootNote: any;
@@ -22,7 +21,7 @@ describe("Search", () => {
});
});
it.skip("simple path match", () => {
xit("simple path match", () => {
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
const searchContext = new SearchContext();
@@ -32,7 +31,7 @@ describe("Search", () => {
expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it.skip("normal search looks also at attributes", () => {
xit("normal search looks also at attributes", () => {
const austria = becca_mocking.note("Austria");
const vienna = becca_mocking.note("Vienna");
@@ -50,7 +49,7 @@ describe("Search", () => {
expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
});
it.skip("normal search looks also at type and mime", () => {
xit("normal search looks also at type and mime", () => {
rootNote.child(becca_mocking.note("Effective Java", { type: "book", mime: "" })).child(becca_mocking.note("Hello World.java", { type: "code", mime: "text/x-java" }));
const searchContext = new SearchContext();
@@ -69,7 +68,7 @@ describe("Search", () => {
expect(searchResults.length).toEqual(2);
});
it.skip("only end leafs are results", () => {
xit("only end leafs are results", () => {
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
const searchContext = new SearchContext();
@@ -79,7 +78,7 @@ describe("Search", () => {
expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
});
it.skip("only end leafs are results", () => {
xit("only end leafs are results", () => {
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")));
const searchContext = new SearchContext();
@@ -134,7 +133,7 @@ describe("Search", () => {
expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it.skip("inherited label comparison", () => {
xit("inherited label comparison", () => {
rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic")));
const searchContext = new SearchContext();
@@ -184,7 +183,8 @@ describe("Search", () => {
function test(query: string, expectedResultCount: number) {
const searchResults = searchService.findResultsWithQuery(query, searchContext);
expect(searchResults.length, `Searching for '${query}' unexpectedly returned ${Number(searchResults?.length)} instead of ${expectedResultCount} results. SearchResult: '${JSON.stringify(searchResults)}'`)
expect(searchResults.length)
.withContext(`While searching for ${query} got unexpected result: [${searchResults.join(", ")}]`)
.toEqual(expectedResultCount);
if (expectedResultCount === 1) {
@@ -549,7 +549,7 @@ describe("Search", () => {
expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
});
it.skip("test note.text *=* something", () => {
xit("test note.text *=* something", () => {
const italy = becca_mocking.note("Italy").label("capital", "Rome");
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
@@ -562,7 +562,7 @@ describe("Search", () => {
expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
});
it.skip("test that fulltext does not match archived notes", () => {
xit("test that fulltext does not match archived notes", () => {
const italy = becca_mocking.note("Italy").label("capital", "Rome");
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
@@ -1,8 +1,7 @@
import { describe, it, expect, beforeEach } from "vitest";
import becca_mocking from "../../../spec/support/becca_mocking.js";
import ValueExtractor from "./value_extractor.js";
import becca from "../../becca/becca.js";
import SearchContext from "./search_context.js";
import becca_mocking from "./becca_mocking.js";
import ValueExtractor from "../../src/services/search/value_extractor.js";
import becca from "../../src/becca/becca.js";
import SearchContext from "../../src/services/search/search_context.js";
const dsc = new SearchContext();
+10
View File
@@ -0,0 +1,10 @@
{
"spec_dir": "",
"spec_files": [
"spec/**/*.spec.ts",
"src/**/*.spec.ts"
],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": true
}
-1
View File
@@ -1,4 +1,3 @@
import { describe, it, expect } from "vitest";
import { trimIndentation } from "./utils.js";
describe("Utils", () => {
+1 -3
View File
@@ -1528,9 +1528,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
}
isLaunchBarConfig() {
return this.type === "launcher"
|| ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId)
|| ["_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(this.noteId);
return this.type === "launcher" || ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId);
}
isOptions() {
+1 -2
View File
@@ -116,8 +116,7 @@ export const ALLOWED_NOTE_TYPES = [
"book",
"webView",
"code",
"mindMap",
"geoMap"
"mindMap"
] as const;
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
+1 -8
View File
@@ -36,12 +36,6 @@ interface DateLimits {
maxDate: string;
}
interface SimilarNote {
score: number;
notePath: string[];
noteId: string;
}
function filterUrlValue(value: string) {
return value
.replace(/https?:\/\//gi, "")
@@ -253,7 +247,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): Promise<SimilarNote[] | undefined> {
async function findSimilarNotes(noteId: string) {
const results = [];
let i = 0;
@@ -423,7 +417,6 @@ async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefin
// this takes care of note hoisting
if (!notePath) {
// TODO: This return is suspicious, it should probably be continue
return;
}
+3 -31
View File
@@ -23,7 +23,6 @@ import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
import type { ContextMenuEvent } from "../menus/context_menu.js";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@@ -70,8 +69,7 @@ export interface ExecuteCommandData extends CommandData {
*/
export type CommandMappings = {
"api-log-messages": CommandData;
focusTree: CommandData,
focusOnDetail: CommandData;
focusOnDetail: Required<CommandData>;
focusOnSearchDefinition: Required<CommandData>;
searchNotes: CommandData & {
searchString?: string;
@@ -104,15 +102,12 @@ export type CommandMappings = {
openNoteInNewTab: CommandData;
openNoteInNewSplit: CommandData;
openNoteInNewWindow: CommandData;
hideLeftPane: CommandData;
showLeftPane: CommandData;
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
toggleNoteHoisting: ContextMenuCommandData;
insertNoteAfter: ContextMenuCommandData;
insertChildNote: ContextMenuCommandData;
delete: ContextMenuCommandData;
protectSubtree: ContextMenuCommandData;
unprotectSubtree: ContextMenuCommandData;
openBulkActionsDialog: ContextMenuCommandData;
@@ -197,10 +192,6 @@ export type CommandMappings = {
setZoomFactorAndSave: {
zoomFactor: string;
}
// Geomap
deleteFromMap: { noteId: string },
openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent }
};
type EventMappings = {
@@ -235,15 +226,9 @@ type EventMappings = {
activeContextChanged: {
noteContext: NoteContext;
};
beforeNoteSwitch: {
noteContext: NoteContext;
};
beforeNoteContextRemove: {
ntxIds: string[];
};
noteSwitched: {
noteContext: NoteContext;
notePath: string | null;
notePath: string;
};
noteSwitchedAndActivatedEvent: {
noteContext: NoteContext;
@@ -262,16 +247,12 @@ type EventMappings = {
noteId: string;
};
hoistedNoteChanged: {
noteId: string;
ntxId: string | null;
ntxId: string;
};
contextsReopenedEvent: {
mainNtxId: string;
tabPosition: number;
};
noteDetailRefreshed: {
ntxId?: string | null;
};
noteContextReorderEvent: {
oldMainNtxId: string;
newMainNtxId: string;
@@ -284,15 +265,6 @@ type EventMappings = {
};
exportSvg: {
ntxId: string;
};
geoMapCreateChildNote: {
ntxId: string | null | undefined; // TODO: deduplicate ntxId
};
tabReorder: {
ntxIdsInOrder: string[]
};
refreshNoteList: {
noteId: string;
}
};
+2 -2
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 | undefined {
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
try {
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
@@ -61,7 +61,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
}
}
triggerEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown> | undefined | null {
triggerEvent(name: string, data = {}): Promise<unknown> | undefined | null {
return this.parent?.triggerEvent(name, data);
}
+1 -1
View File
@@ -24,7 +24,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
notePath?: string | null;
noteId?: string | null;
parentNoteId?: string | null;
private parentNoteId?: string | null;
viewScope?: ViewScope;
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
@@ -9,8 +9,6 @@ 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();
@@ -46,9 +44,10 @@ if (utils.isElectron()) {
}
function initOnElectron() {
const electron: typeof Electron = utils.dynamicRequire("electron");
const electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
const electronRemote: typeof ElectronRemote = utils.dynamicRequire("@electron/remote");
const electronRemote = utils.dynamicRequire("@electron/remote");
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
@@ -59,7 +58,7 @@ function initOnElectron() {
}
}
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
function initTitleBarButtons(style, currentWindow) {
if (window.glob.platform === "win32") {
const applyWindowsOverlay = () => {
const color = style.getPropertyValue("--native-titlebar-background");
@@ -82,14 +81,9 @@ function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron
}
}
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
function initTransparencyEffects(style, currentWindow) {
if (window.glob.platform === "win32") {
const material = style.getPropertyValue("--background-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);
}
currentWindow.setBackgroundMaterial(material);
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default class FBlob {
/**
* @throws Error in case of invalid JSON
*/
getJsonContent<T>(): T | null {
getJsonContent(): unknown {
if (!this.content || !this.content.trim()) {
return null;
}
+9 -10
View File
@@ -27,8 +27,7 @@ const NOTE_TYPE_ICONS = {
launcher: "bx bx-link",
doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap",
geoMap: "bx bx-map-alt"
mindMap: "bx bx-sitemap"
};
/**
@@ -36,12 +35,12 @@ const NOTE_TYPE_ICONS = {
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap";
type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap";
export interface NotePathRecord {
interface NotePathRecord {
isArchived: boolean;
isInHoistedSubTree: boolean;
isSearch?: boolean;
isSearch: boolean;
notePath: string[];
isHidden: boolean;
}
@@ -402,14 +401,14 @@ class FNote {
return notePaths;
}
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
getSortedNotePathRecords(hoistedNoteId = "root") {
const isHoistedRoot = hoistedNoteId === "root";
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
const notePaths = this.getAllNotePaths().map((path) => ({
notePath: path,
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
isSearch: path.find((noteId) => froca.notes[noteId].type === "search"),
isHidden: path.includes("_hidden")
}));
@@ -516,10 +515,10 @@ class FNote {
}
/**
* @param name - label name to filter
* @param [name] - label name to filter
* @returns all note's labels (attributes with type label), including inherited ones
*/
getOwnedLabels(name?: string) {
getOwnedLabels(name: string) {
return this.getOwnedAttributes(LABEL, name);
}
-2
View File
@@ -85,7 +85,6 @@ import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -201,7 +200,6 @@ export default class DesktopLayout {
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new GeoMapButtons())
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
-7
View File
@@ -1,6 +1,5 @@
import type { CommandNames } from "../components/app_context.js";
import keyboardActionService from "../services/keyboard_actions.js";
import note_tooltip from "../services/note_tooltip.js";
import utils from "../services/utils.js";
interface ContextMenuOptions<T extends CommandNames> {
@@ -9,8 +8,6 @@ interface ContextMenuOptions<T extends CommandNames> {
orientation?: "left";
selectMenuItemHandler: MenuHandler<T>;
items: MenuItem<T>[];
/** On mobile, if set to `true` then the context menu is shown near the element. If `false` (default), then the context menu is shown at the bottom of the screen. */
forcePositionOnMobile?: boolean;
}
interface MenuSeparatorItem {
@@ -32,7 +29,6 @@ export interface MenuCommandItem<T extends CommandNames> {
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
class ContextMenu {
private $widget: JQuery<HTMLElement>;
@@ -58,15 +54,12 @@ class ContextMenu {
async show<T extends CommandNames>(options: ContextMenuOptions<T>) {
this.options = options;
note_tooltip.dismissAllTooltips();
if (this.$widget.hasClass("show")) {
// The menu is already visible. Hide the menu then open it again
// at the new location to re-trigger the opening animation.
await this.hide();
}
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
this.$cover.addClass("show");
$("body").addClass("context-menu-shown");
@@ -34,8 +34,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleItem = (parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers");
const isAvailableItem = (parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers");
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers";
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers";
const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
+24 -32
View File
@@ -1,44 +1,36 @@
import { t } from "../services/i18n.js";
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
import appContext, { type CommandNames } from "../components/app_context.js";
import contextMenu from "./context_menu.js";
import appContext from "../components/app_context.js";
import type { ViewScope } from "../services/link.js";
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
function openContextMenu(notePath: string, e: PointerEvent | JQuery.ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: getItems(),
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
items: [
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
],
selectMenuItemHandler: ({ command }) => {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
}
if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") {
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
}
}
});
}
function getItems(): MenuItem<CommandNames>[] {
return [
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
];
}
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
}
if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") {
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
}
}
export default {
getItems,
handleLinkContextMenuItem,
openContextMenu
};
+1 -5
View File
@@ -1,16 +1,12 @@
import type { EntityRowNames } from "./services/load_results.js";
interface Entity {
isDeleted?: boolean;
}
// TODO: Deduplicate with src/services/entity_changes_interface.ts
export interface EntityChange {
id?: number | null;
noteId?: string;
entityName: EntityRowNames;
entityId: string;
entity?: Entity;
entity?: any;
positions?: Record<string, number>;
hash: string;
utcDateChanged?: string;
+7 -9
View File
@@ -290,16 +290,14 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) {
return;
}
if (ec.entity) {
if (attachment) {
attachment.update(ec.entity as FAttachmentRow);
} else {
const attachmentRow = ec.entity as FAttachmentRow;
const note = froca.notes[attachmentRow.ownerId];
if (attachment) {
attachment.update(ec.entity as FAttachmentRow);
} else {
const attachmentRow = ec.entity as FAttachmentRow;
const note = froca.notes[attachmentRow.ownerId];
if (note?.attachments) {
note.attachments.push(new FAttachment(froca, attachmentRow));
}
if (note?.attachments) {
note.attachments.push(new FAttachment(froca, attachmentRow));
}
}
+20 -6
View File
@@ -68,10 +68,26 @@ const WHEEL_ZOOM: Library = {
js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
};
const FORCE_GRAPH: Library = {
js: ["node_modules/force-graph/dist/force-graph.min.js"]
};
const MERMAID: Library = {
js: ["node_modules/mermaid/dist/mermaid.min.js"]
};
/**
* The ELK extension of Mermaid.js, which supports more advanced layouts.
* See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
*/
const MERMAID_ELK: Library = {
js: ["libraries/mermaid-elk/elk.min.js"]
};
const EXCALIDRAW: Library = {
js: ["node_modules/react/umd/react.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"]
};
const MARKJS: Library = {
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
};
@@ -106,10 +122,6 @@ const HIGHLIGHT_JS: Library = {
}
};
const LEAFLET: Library = {
css: [ "node_modules/leaflet/dist/leaflet.css" ],
}
async function requireLibrary(library: Library) {
if (library.css) {
library.css.map((cssUrl) => requireCss(cssUrl));
@@ -197,9 +209,11 @@ export default {
CALENDAR_WIDGET,
KATEX,
WHEEL_ZOOM,
FORCE_GRAPH,
MERMAID,
MERMAID_ELK,
EXCALIDRAW,
MARKJS,
I18NEXT,
HIGHLIGHT_JS,
LEAFLET
HIGHLIGHT_JS
};
+8 -5
View File
@@ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
return goToLinkExt(evt, hrefLink, $link);
}
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement>) {
if (hrefLink?.startsWith("data:")) {
return true;
}
@@ -242,17 +242,20 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
evt.preventDefault();
evt.stopPropagation();
if (hrefLink?.startsWith("#fn") && $link) {
if (hrefLink?.startsWith("#fn")) {
return handleFootnote(hrefLink, $link);
}
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = ("which" in evt && evt.which === 1);
const isMiddleClick = ("which" in evt && evt.which === 2);
const isLeftClick = evt.which === 1;
const isMiddleClick = evt.which === 2;
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
const leftClick = evt.which === 1;
const middleClick = evt.which === 2;
if (notePath) {
if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
@@ -273,7 +276,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
if (openInNewTab || (withinEditLink && (leftClick || middleClick)) || (outsideOfCKEditor && (leftClick || middleClick))) {
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
window.open(hrefLink, "_blank");
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
+1 -2
View File
@@ -8,7 +8,6 @@ interface NoteRow {
}
interface BranchRow {
noteId?: string;
branchId: string;
componentId: string;
parentNoteId?: string;
@@ -158,7 +157,7 @@ export default class LoadResults {
return Object.keys(this.noteIdToComponentId);
}
isNoteReloaded(noteId: string | undefined, componentId: string | null = null) {
isNoteReloaded(noteId: string, componentId = null) {
if (!noteId) {
return false;
}
+4 -1
View File
@@ -1,3 +1,5 @@
import library_loader from "./library_loader.js";
let elkLoaded = false;
/**
@@ -20,6 +22,7 @@ export async function loadElkIfNeeded(mermaidContent: string) {
});
if (parsedContent?.config?.layout === "elk") {
elkLoaded = true;
mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default);
await library_loader.requireLibrary(library_loader.MERMAID_ELK);
mermaid.registerLayoutLoaders(MERMAID_ELK);
}
}
@@ -1,5 +1,3 @@
// TODO: deduplicate with /src/services/import/mime_type_definitions.ts
/**
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
*/
+6 -7
View File
@@ -18,11 +18,11 @@ function setupGlobalTooltip() {
return;
}
dismissAllTooltips();
cleanUpTooltips();
});
}
function dismissAllTooltips() {
function cleanUpTooltips() {
$(".note-tooltip").remove();
}
@@ -102,12 +102,12 @@ async function mouseEnterHandler(this: HTMLElement) {
customClass: linkId
});
dismissAllTooltips();
cleanUpTooltips();
$(this).tooltip("show");
// Dismiss the tooltip immediately if a link was clicked inside the tooltip.
$(`.${tooltipClass} a`).on("click", (e) => {
dismissAllTooltips();
cleanUpTooltips();
});
// the purpose of the code below is to:
@@ -117,7 +117,7 @@ async function mouseEnterHandler(this: HTMLElement) {
const checkTooltip = () => {
if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) {
// cursor is neither over the link nor over the tooltip, user likely is not interested
dismissAllTooltips();
cleanUpTooltips();
} else {
setTimeout(checkTooltip, 1000);
}
@@ -172,6 +172,5 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) {
export default {
setupGlobalTooltip,
setupElementTooltip,
dismissAllTooltips
setupElementTooltip
};
+1 -2
View File
@@ -18,8 +18,7 @@ async function getNoteTypeItems(command?: NoteTypeCommandNames) {
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }
];
const templateNoteIds = await server.get<string[]>("search-templates");
+3 -7
View File
@@ -36,14 +36,10 @@ class Options {
getInt(key: string) {
const value = this.arr?.[key];
if (typeof value === "number") {
return value;
if (typeof value !== "string") {
return null;
}
if (typeof value == "string") {
return parseInt(value);
}
console.warn("Attempting to read int for unsupported value: ", value);
return null;
return parseInt(value);
}
getFloat(key: string) {
-8
View File
@@ -43,14 +43,6 @@ export default class SpacedUpdate {
return allSaved;
}
/**
* Normally {@link scheduleUpdate()} would actually trigger the update only once per {@link updateInterval}. If the method is called 200 times within 20s, it will execute only 20 times.
* Sometimes, if the updates are continuous this would cause a performance impact. Resetting the time ensures that the calls to {@link triggerUpdate} have stopped before actually triggering an update.
*/
resetUpdateTimer() {
this.lastUpdated = Date.now();
}
triggerUpdate() {
if (!this.changed) {
return;
+2 -4
View File
@@ -97,7 +97,7 @@ function isMac() {
return navigator.platform.indexOf("Mac") > -1;
}
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) {
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}
@@ -308,9 +308,7 @@ function dynamicRequire(moduleName: string) {
if (typeof __non_webpack_require__ !== "undefined") {
return __non_webpack_require__(moduleName);
} else {
// explicitly pass as string and not as expression to suppress webpack warning
// 'Critical dependency: the request of a dependency is an expression'
return require(`${moduleName}`);
return require(moduleName);
}
}
@@ -3,7 +3,7 @@
*
* @param noteId of the given note to be fetched. If false, fetches current note.
*/
async function fetchNote(noteId: string | null = null) {
async function fetchNote(noteId = null) {
if (!noteId) {
noteId = document.body.getAttribute("data-note-id");
}
@@ -25,9 +25,3 @@ 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
});
+3 -3
View File
@@ -43,7 +43,6 @@ interface CustomGlobals {
appCssNoteIds: string[];
triliumVersion: string;
TRILIUM_SAFE_MODE: boolean;
platform?: typeof process.platform;
}
type RequireMethod = (moduleName: string) => any;
@@ -156,12 +155,13 @@ declare global {
registerLayoutLoaders(loader: MermaidLoader);
parse(content: string, opts: {
suppressErrors: true
}): Promise<{
}): {
config: {
layout: string;
}
}>
}
};
var MERMAID_ELK: MermaidLoader;
var CKEditor: {
BalloonEditor: {
+2 -2
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(): boolean | null | undefined {
isEnabled() {
return true;
}
@@ -205,7 +205,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
*/
doRender() {}
toggleInt(show: boolean | null | undefined) {
toggleInt(show: boolean) {
this.$widget.toggleClass("hidden-int", !show);
}
@@ -86,7 +86,7 @@ const TPL = `
position: relative;
top: 3px;
font-size: 120%;
margin-right: 6px;
margin-right: 5px;
}
body.mobile .global-menu .dropdown-submenu .dropdown-menu {
@@ -162,7 +162,8 @@ const TPL = `
<li class="dropdown-item dropdown-submenu">
<span class="dropdown-toggle">
<span class="bx bx-chip"></span>${t("global_menu.advanced")}
<span class="bx bx-chip"></span>
${t("global_menu.advanced")}
</span>
<ul class="dropdown-menu">
@@ -2,11 +2,9 @@ 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: boolean) {
constructor(isHorizontalLayout) {
super();
this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
@@ -34,7 +32,7 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
splitService.setupLeftPaneResizer(options.is("leftPaneVisible"));
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
entitiesReloadedEvent({ loadResults }) {
if (loadResults.isOptionReloaded("leftPaneVisible")) {
this.refreshIcon();
}
@@ -42,7 +42,7 @@ const TPL = `
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
<span class="bx bx-paperclip"></span> ${t("note_actions.convert_into_attachment")}
</li>
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
<span class="bx bx-extension"></span> ${t("note_actions.re_render_note")}<kbd data-command="renderActiveNote"></kbd>
</li>
@@ -54,15 +54,15 @@ const TPL = `
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd></li>
<div class="dropdown-divider"></div>
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t("note_actions.import_files")}</li>
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t("note_actions.export_note")}</li>
<div class="dropdown-divider"></div>
@@ -79,7 +79,7 @@ const TPL = `
<span class="bx bx-code"></span> ${t("note_actions.note_source")}<kbd data-command="showNoteSource"></kbd>
</li>
<div class="dropdown-divider"></div>
@@ -89,10 +89,10 @@ const TPL = `
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t("note_actions.delete_note")}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
<span class="bx bx-paperclip"></span> ${t("note_actions.note_attachments")}<kbd data-command="showAttachments"></kbd>
</li>
@@ -154,7 +154,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type));
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
this.toggleDisabled(this.$printActiveNoteButton, ["text", "code"].includes(note.type));
@@ -1,9 +1,8 @@
import options from "../../services/options.js";
import FlexContainer from "./flex_container.js";
import appContext, { type EventData } from "../../components/app_context.js";
import type Component from "../../components/component.js";
import appContext from "../../components/app_context.js";
export default class LeftPaneContainer extends FlexContainer<Component> {
export default class LeftPaneContainer extends FlexContainer {
constructor() {
super("column");
@@ -16,13 +15,13 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
return super.isEnabled() && options.is("leftPaneVisible");
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
entitiesReloadedEvent({ loadResults }) {
if (loadResults.isOptionReloaded("leftPaneVisible")) {
const visible = this.isEnabled();
this.toggleInt(visible);
if (visible) {
this.triggerEvent("focusTree", {});
this.triggerEvent("focusTree");
} else {
const activeNoteContext = appContext.tabManager.getActiveContext();
this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId });
@@ -1,10 +1,6 @@
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">
@@ -12,11 +8,11 @@ const TPL = `
.ribbon-container {
margin-bottom: 5px;
}
.ribbon-top-row {
display: flex;
}
.ribbon-tab-container {
display: flex;
flex-direction: row;
@@ -25,10 +21,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;
@@ -40,7 +36,7 @@ const TPL = `
position: relative;
top: 3px;
}
.ribbon-tab-title.active {
color: var(--main-text-color);
border-bottom: 3px solid var(--main-text-color);
@@ -48,7 +44,7 @@ const TPL = `
overflow: hidden;
text-overflow: ellipsis;
}
.ribbon-tab-title:hover {
cursor: pointer;
}
@@ -56,11 +52,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;
@@ -68,41 +64,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;
}
@@ -112,21 +108,11 @@ 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();
@@ -136,10 +122,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: NoteContextAwareWidget) { // TODO: Base class
ribbon(widget) {
super.child(widget);
this.ribbonWidgets.push(widget);
@@ -147,7 +133,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
return this;
}
button(widget: CommandButtonWidget) {
button(widget) {
super.child(widget);
this.buttonWidgets.push(widget);
@@ -177,7 +163,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
});
}
toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
toggleRibbonTab($ribbonTitle, refreshActiveTab = true) {
const activate = !$ribbonTitle.hasClass("active");
this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
@@ -195,15 +181,14 @@ export default class RibbonContainer extends NoteContextAwareWidget {
const activeChild = this.getActiveRibbonWidget();
if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
if (activeChild && (refreshActiveTab || !wasAlreadyActive)) {
const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
if (refreshActiveTab) {
if (handleEventPromise) {
handleEventPromise.then(() => (activeChild as any).focus()); // TODO: Base class
handleEventPromise.then(() => activeChild.focus?.());
} else {
// TODO: Base class
(activeChild as any)?.focus();
activeChild.focus?.();
}
}
}
@@ -218,7 +203,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
await super.noteSwitched();
}
async refreshWithNote(note: FNote, noExplicitActivation = false) {
async refreshWithNote(note, noExplicitActivation = false) {
this.lastNoteType = note.type;
let $ribbonTabToActivate, $lastActiveRibbon;
@@ -226,8 +211,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
this.$tabContainer.empty();
for (const ribbonWidget of this.ribbonWidgets) {
// TODO: Base class for ribbon widget
const ret = await (ribbonWidget as any).getTitle(note);
const ret = await ribbonWidget.getTitle(note);
if (!ret.show) {
continue;
@@ -235,8 +219,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 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
.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))
.append(" ")
.append($('<span class="ribbon-tab-title-label">').text(ret.title));
@@ -254,7 +238,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
keyboardActionsService.getActions().then((actions) => {
this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
title: () => {
title: function () {
const toggleCommandName = $(this).attr("data-toggle-command");
const action = actions.find((act) => act.actionName === toggleCommandName);
const title = $(this).attr("data-title");
@@ -262,7 +246,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
if (action && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
} else {
return title ?? "";
return title;
}
}
});
@@ -279,27 +263,27 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
isRibbonTabActive(name: string) {
isRibbonTabActive(name) {
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
return $ribbonComponent.hasClass("active");
}
ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
ensureOwnedAttributesAreOpen(ntxId) {
if (this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
this.toggleRibbonTabWithName("ownedAttributes", ntxId);
}
}
addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
addNewLabelEvent({ ntxId }) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
addNewRelationEvent({ ntxId }) {
this.ensureOwnedAttributesAreOpen(ntxId);
}
toggleRibbonTabWithName(name: string, ntxId?: string) {
toggleRibbonTabWithName(name, ntxId) {
if (!this.isNoteContext(ntxId)) {
return false;
}
@@ -311,23 +295,23 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
handleEvent(name, data) {
const PREFIX = "toggleRibbonTab";
if (name.startsWith(PREFIX)) {
let componentName = name.substr(PREFIX.length);
componentName = componentName[0].toLowerCase() + componentName.substr(1);
this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
this.toggleRibbonTabWithName(componentName, data.ntxId);
} else {
return super.handleEvent(name, data);
}
}
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
async handleEventInChildren(name, data) {
if (["activeContextChanged", "setNoteContext"].includes(name)) {
// won't trigger .refresh();
await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
await super.handleEventInChildren("setNoteContext", data);
} else if (this.isEnabled() || name === "initialRenderComplete") {
const activeRibbonWidget = this.getActiveRibbonWidget();
@@ -342,12 +326,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (!this.note) {
return;
}
if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
entitiesReloadedEvent({ loadResults }) {
if (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;
@@ -358,7 +338,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
async noteTypeMimeChangedEvent() {
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,10 +1,6 @@
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">
@@ -12,11 +8,7 @@ const TPL = `
.editability-dropdown {
width: 300px;
}
.editability-dropdown .dropdown-item {
display: block !important;
}
.editability-dropdown .dropdown-item div {
font-size: small;
color: var(--muted-text-color);
@@ -31,7 +23,7 @@ const TPL = `
<a class="dropdown-item" href="#" data-editability="auto">
<span class="check">&check;</span>
${t("editability_select.auto")}
<div>${t("editability_select.note_is_editable")}</div>
<div>${t("editability_select.note_is_editable")}</div>
</a>
<a class="dropdown-item" href="#" data-editability="readOnly">
<span class="check">&check;</span>
@@ -48,15 +40,9 @@ 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");
@@ -66,28 +52,24 @@ 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 && editability !== "auto") {
if (editability !== "auto") {
await attributeService.addLabel(this.noteId, editability);
}
});
}
async refreshWithNote(note: FNote) {
let editability: Editability = "auto";
async refreshWithNote(note) {
let 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";
}
@@ -103,7 +85,7 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
this.$editabilityActiveDesc.text(labels[editability]);
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
entitiesReloadedEvent({ loadResults }) {
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
this.refresh();
}
@@ -1,42 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js"
const TPL = `\
<div class="geo-map-buttons">
<style>
.geo-map-buttons {
display: flex;
gap: 10px;
}
.leaflet-pane {
z-index: 50;
}
.geo-map-buttons {
contain: none;
background: var(--main-background-color);
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
border-radius: 4px;
}
</style>
<button type="button"
class="geo-map-create-child-note floating-button btn bx bx-folder-plus"
title="${t("geo-map.create-child-note-title")}" />
</div>`;
export default class GeoMapButtons extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && this.note?.type === "geoMap";
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.find(".geo-map-create-child-note").on("click", () => this.triggerEvent("geoMapCreateChildNote", { ntxId: this.ntxId }));
}
}
@@ -13,16 +13,16 @@ const TPL = `
<button type="button"
class="relation-map-create-child-note floating-button btn bx bx-folder-plus"
title="${t("relation_map_buttons.create_child_note_title")}"></button>
<button type="button"
class="relation-map-reset-pan-zoom floating-button btn bx bx-crop"
title="${t("relation_map_buttons.reset_pan_zoom_title")}"></button>
<div class="btn-group">
<button type="button"
class="relation-map-zoom-in floating-button btn bx bx-zoom-in"
title="${t("relation_map_buttons.zoom_in_title")}"></button>
<button type="button"
class="relation-map-zoom-out floating-button btn bx bx-zoom-out"
title="${t("relation_map_buttons.zoom_out_title")}"></button>
@@ -43,7 +43,6 @@ export default class RelationMapButtons extends NoteContextAwareWidget {
this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out");
this.$resetPanZoomButton = this.$widget.find(".relation-map-reset-pan-zoom");
// TODO: Deduplicate object creation here.
this.$createChildNote.on("click", () => this.triggerEvent("relationMapCreateChildNote", { ntxId: this.ntxId }));
this.$resetPanZoomButton.on("click", () => this.triggerEvent("relationMapResetPanZoom", { ntxId: this.ntxId }));
@@ -7,7 +7,6 @@ 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">
@@ -15,7 +14,7 @@ const TPL = `
.backlinks-widget {
position: relative;
}
.backlinks-ticker {
border-radius: 10px;
border-color: var(--main-border-color);
@@ -26,11 +25,11 @@ const TPL = `
justify-content: space-between;
align-items: center;
}
.backlinks-count {
cursor: pointer;
}
.backlinks-items {
z-index: 10;
position: absolute;
@@ -43,41 +42,29 @@ 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");
@@ -86,7 +73,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.$count.on("click", () => {
this.$items.toggle();
this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10);
this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
if (this.$items.is(":visible")) {
this.renderBacklinks();
@@ -96,7 +83,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.contentSized();
}
async refreshWithNote(note: FNote) {
async refreshWithNote(note) {
this.clearItems();
if (this.noteContext?.viewScope?.viewMode !== "default") {
@@ -105,8 +92,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
}
// can't use froca since that would count only relations from loaded notes
// TODO: Deduplicate response type
const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`);
const resp = await server.get(`note-map/${this.noteId}/backlink-count`);
if (!resp || !resp.count) {
this.toggle(false);
@@ -120,7 +106,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
);
}
toggle(show: boolean) {
toggle(show) {
this.$widget.toggleClass("hidden-no-content", !show);
}
@@ -135,7 +121,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
this.$items.empty();
const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`);
const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
if (!backlinks.length) {
return;
@@ -157,7 +143,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);
-57
View File
@@ -1,57 +0,0 @@
import type { Map } from "leaflet";
import library_loader from "../services/library_loader.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `\
<div class="geo-map-widget">
<style>
.note-detail-geo-map,
.geo-map-widget,
.geo-map-container {
height: 100%;
overflow: hidden;
}
</style>
<div class="geo-map-container"></div>
</div>`
export type Leaflet = typeof import("leaflet");
export type InitCallback = ((L: Leaflet) => void);
export default class GeoMapWidget extends NoteContextAwareWidget {
map?: Map;
$container!: JQuery<HTMLElement>;
private initCallback?: InitCallback;
constructor(widgetMode: "type", initCallback?: InitCallback) {
super();
this.initCallback = initCallback;
}
doRender() {
this.$widget = $(TPL);
this.$container = this.$widget.find(".geo-map-container");
library_loader.requireLibrary(library_loader.LEAFLET)
.then(async () => {
const L = (await import("leaflet")).default;
const map = L.map(this.$container[0], {
worldCopyJump: true
});
this.map = map;
if (this.initCallback) {
this.initCallback(L);
}
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
});
}
}

Some files were not shown because too many files have changed in this diff Show More