Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dc546605b | |||
| 62cda1bd24 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
src/public/app-dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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 }) => {
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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/");
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -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");
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
@@ -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("./"));
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
@@ -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()
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"spec_dir": "",
|
||||
"spec_files": [
|
||||
"spec/**/*.spec.ts",
|
||||
"src/**/*.spec.ts"
|
||||
],
|
||||
"helpers": ["helpers/**/*.js"],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { trimIndentation } from "./utils.js";
|
||||
|
||||
describe("Utils", () => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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">✓</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">✓</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);
|
||||
@@ -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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||