mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-11 02:42:27 +08:00
Merge remote-tracking branch 'origin/develop' into share-fix
This commit is contained in:
commit
59d8def2c5
24
.github/workflows/main.yml
vendored
24
.github/workflows/main.yml
vendored
@ -72,9 +72,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}}
|
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}}
|
||||||
path: upload/*.${{ matrix.os.extension }}
|
path: upload/*.${{ matrix.os.extension }}
|
||||||
build_linux_server-x64:
|
build_linux_server:
|
||||||
name: Build Linux Server x86_64
|
name: Build Linux Server
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [x64, arm64]
|
||||||
|
include:
|
||||||
|
- arch: x64
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
- arch: arm64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
@ -84,17 +93,18 @@ jobs:
|
|||||||
cache: "npm"
|
cache: "npm"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run Linux server build (x86_64)
|
- name: Run Linux server build
|
||||||
|
env:
|
||||||
|
MATRIX_ARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
npm run update-build-info
|
npm run update-build-info
|
||||||
./bin/build-server.sh
|
./bin/build-server.sh
|
||||||
- name: Prepare artifacts
|
- name: Prepare artifacts
|
||||||
if: runner.os != 'windows'
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p upload
|
mkdir -p upload
|
||||||
file=$(find dist -name '*.tar.xz' -print -quit)
|
file=$(find dist -name '*.tar.xz' -print -quit)
|
||||||
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
|
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: TriliumNextNotes linux server x64
|
name: TriliumNextNotes linux server ${{ matrix.arch }}
|
||||||
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
|
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
coverage/
|
||||||
src/public/app-dist/
|
src/public/app-dist/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
@ -2,21 +2,57 @@
|
|||||||
|
|
||||||
set -e # Fail on any command error
|
set -e # Fail on any command error
|
||||||
|
|
||||||
PKG_DIR=dist/trilium-linux-x64-server
|
# 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
|
||||||
NODE_VERSION=20.15.1
|
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" ]
|
if [ "$1" != "DONTCOPY" ]
|
||||||
then
|
then
|
||||||
./bin/copy-trilium.sh $PKG_DIR
|
# Need to modify copy-trilium.sh to accept the target directory
|
||||||
|
./bin/copy-trilium.sh "$PKG_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd dist
|
cd dist
|
||||||
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz
|
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||||
tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz
|
tar xfJ node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||||
rm node-v${NODE_VERSION}-linux-x64.tar.xz
|
rm node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
mv dist/node-v${NODE_VERSION}-linux-x64 $PKG_DIR/node
|
mv dist/node-v${NODE_VERSION}-linux-${NODE_FILENAME} $PKG_DIR/node
|
||||||
|
|
||||||
rm -r $PKG_DIR/node/lib/node_modules/npm
|
rm -r $PKG_DIR/node/lib/node_modules/npm
|
||||||
rm -r $PKG_DIR/node/include/node
|
rm -r $PKG_DIR/node/include/node
|
||||||
@ -37,4 +73,4 @@ VERSION=`jq -r ".version" package.json`
|
|||||||
|
|
||||||
cd dist
|
cd dist
|
||||||
|
|
||||||
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server
|
tar cJf trilium-linux-${ARCH}-server-${VERSION}.tar.xz trilium-linux-${ARCH}-server
|
||||||
|
@ -76,7 +76,6 @@ const copy = async () => {
|
|||||||
"node_modules/@excalidraw/excalidraw/dist/",
|
"node_modules/@excalidraw/excalidraw/dist/",
|
||||||
"node_modules/katex/dist/",
|
"node_modules/katex/dist/",
|
||||||
"node_modules/dayjs/",
|
"node_modules/dayjs/",
|
||||||
"node_modules/force-graph/dist/",
|
|
||||||
"node_modules/boxicons/css/",
|
"node_modules/boxicons/css/",
|
||||||
"node_modules/boxicons/fonts/",
|
"node_modules/boxicons/fonts/",
|
||||||
"node_modules/mermaid/dist/",
|
"node_modules/mermaid/dist/",
|
||||||
|
@ -12,7 +12,7 @@ function getDataKey(password: any) {
|
|||||||
|
|
||||||
const encryptedDataKey = getOption("encryptedDataKey");
|
const encryptedDataKey = getOption("encryptedDataKey");
|
||||||
|
|
||||||
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
|
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey);
|
||||||
|
|
||||||
return decryptedDataKey;
|
return decryptedDataKey;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -16,7 +16,7 @@ function decryptString(dataKey: any, cipherText: any) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrypt(key: any, cipherText: any, ivLength = 13) {
|
function decrypt(key: any, cipherText: any) {
|
||||||
if (cipherText === null) {
|
if (cipherText === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -27,6 +27,8 @@ function decrypt(key: any, cipherText: any, ivLength = 13) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
|
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 iv = cipherTextBufferWithIv.slice(0, ivLength);
|
||||||
|
|
||||||
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
||||||
|
27
e2e/help.spec.ts
Normal file
27
e2e/help.spec.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { test, expect, Page } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
test("Help popup", async ({ page, context }) => {
|
||||||
|
page.setDefaultTimeout(15_000);
|
||||||
|
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
const popupPromise = page.waitForEvent("popup");
|
||||||
|
await app.currentNoteSplit.press("F1");
|
||||||
|
await page.getByRole("link", { name: "online↗" }).click();
|
||||||
|
const popup = await popupPromise;
|
||||||
|
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Complete help in search", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
await app.launcherBar.locator(".bx-search").first().click();
|
||||||
|
await app.currentNoteSplit.locator(".search-settings .bx-help-circle").click();
|
||||||
|
const popupPromise = page.waitForEvent("popup");
|
||||||
|
await page.getByRole("link", { name: "complete help on search syntax" }).click();
|
||||||
|
const popup = await popupPromise;
|
||||||
|
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
|
||||||
|
});
|
67
e2e/note_types/mermaid.spec.ts
Normal file
67
e2e/note_types/mermaid.spec.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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, Page } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import App from "../support/app";
|
import App from "../support/app";
|
||||||
|
|
||||||
test("displays simple map", async ({ page, context }) => {
|
test("displays simple map", async ({ page, context }) => {
|
9
e2e/note_types/note_map.spec.ts
Normal file
9
e2e/note_types/note_map.spec.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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,3 +49,20 @@ test("Highlights list is displayed", async ({ page, context }) => {
|
|||||||
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
|
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");
|
||||||
|
});
|
||||||
|
@ -13,6 +13,7 @@ export default class App {
|
|||||||
|
|
||||||
readonly tabBar: Locator;
|
readonly tabBar: Locator;
|
||||||
readonly noteTree: Locator;
|
readonly noteTree: Locator;
|
||||||
|
readonly launcherBar: Locator;
|
||||||
readonly currentNoteSplit: Locator;
|
readonly currentNoteSplit: Locator;
|
||||||
readonly sidebar: Locator;
|
readonly sidebar: Locator;
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ export default class App {
|
|||||||
|
|
||||||
this.tabBar = page.locator(".tab-row-widget-container");
|
this.tabBar = page.locator(".tab-row-widget-container");
|
||||||
this.noteTree = page.locator(".tree-wrapper");
|
this.noteTree = page.locator(".tree-wrapper");
|
||||||
|
this.launcherBar = page.locator("#launcher-container");
|
||||||
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)")
|
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)")
|
||||||
this.sidebar = page.locator("#right-pane");
|
this.sidebar = page.locator("#right-pane");
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@ -1,23 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
@ -1,17 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
1
libraries/mermaid-elk/elk.min.js
vendored
1
libraries/mermaid-elk/elk.min.js
vendored
File diff suppressed because one or more lines are too long
13
libraries/mermaid-elk/package-lock.json
generated
13
libraries/mermaid-elk/package-lock.json
generated
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mermaid-elk",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "mermaid-elk",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"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": {}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
// 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("./"));
|
|
2658
package-lock.json
generated
2658
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -35,11 +35,10 @@
|
|||||||
"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-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-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",
|
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
|
||||||
"webpack": "cross-env node --import ./loader-register.js node_modules/webpack/bin/webpack.js -c webpack.config.ts",
|
"webpack": "tsx node_modules/webpack/bin/webpack.js -c webpack.config.ts",
|
||||||
"test-playwright": "playwright test",
|
"test-playwright": "playwright test",
|
||||||
"test-jasmine": "cross-env TRILIUM_DATA_DIR=./data-test tsx ./node_modules/jasmine/bin/jasmine.js",
|
"test": "cross-env TRILIUM_DATA_DIR=./data-test vitest",
|
||||||
"test-es6": "tsx -r esm spec-es6/attribute_parser.spec.ts",
|
"test-coverage": "cross-env TRILIUM_DATA_DIR=./data-test vitest --coverage",
|
||||||
"test": "npm run test-jasmine && npm run test-es6",
|
|
||||||
"start-electron-forge": "npm run prepare-dist && electron-forge start",
|
"start-electron-forge": "npm run prepare-dist && electron-forge start",
|
||||||
"make-electron": "npm run webpack && npm run prepare-dist && electron-forge make",
|
"make-electron": "npm run webpack && npm run prepare-dist && electron-forge make",
|
||||||
"package-electron": "electron-forge package",
|
"package-electron": "electron-forge package",
|
||||||
@ -89,8 +88,8 @@
|
|||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-rate-limit": "7.5.0",
|
"express-rate-limit": "7.5.0",
|
||||||
"express-session": "1.18.1",
|
"express-session": "1.18.1",
|
||||||
"force-graph": "1.47.2",
|
"force-graph": "1.49.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.3.0",
|
||||||
"helmet": "8.0.0",
|
"helmet": "8.0.0",
|
||||||
"html": "1.0.0",
|
"html": "1.0.0",
|
||||||
"html2plaintext": "2.1.4",
|
"html2plaintext": "2.1.4",
|
||||||
@ -110,7 +109,7 @@
|
|||||||
"jquery.fancytree": "2.38.4",
|
"jquery.fancytree": "2.38.4",
|
||||||
"jsdom": "26.0.0",
|
"jsdom": "26.0.0",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.19",
|
"katex": "0.16.20",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "15.0.6",
|
"marked": "15.0.6",
|
||||||
@ -189,10 +188,10 @@
|
|||||||
"@types/ws": "8.5.13",
|
"@types/ws": "8.5.13",
|
||||||
"@types/xml2js": "0.4.14",
|
"@types/xml2js": "0.4.14",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
|
"@vitest/coverage-v8": "3.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"electron": "34.0.0",
|
"electron": "34.0.0",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"iconsur": "1.7.0",
|
|
||||||
"jasmine": "5.5.0",
|
"jasmine": "5.5.0",
|
||||||
"jsdoc": "4.0.4",
|
"jsdoc": "4.0.4",
|
||||||
"lorem-ipsum": "2.0.8",
|
"lorem-ipsum": "2.0.8",
|
||||||
@ -200,11 +199,11 @@
|
|||||||
"prettier": "3.4.2",
|
"prettier": "3.4.2",
|
||||||
"rcedit": "4.0.1",
|
"rcedit": "4.0.1",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
"ts-node": "10.9.2",
|
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
"typedoc": "0.27.6",
|
"typedoc": "0.27.6",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
|
"vitest": "3.0.2",
|
||||||
"webpack": "5.97.1",
|
"webpack": "5.97.1",
|
||||||
"webpack-cli": "6.0.1",
|
"webpack-cli": "6.0.1",
|
||||||
"webpack-dev-middleware": "7.4.2"
|
"webpack-dev-middleware": "7.4.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as attributeParser from "../src/public/app/services/attribute_parser.js";
|
import { describe, it, expect } from "vitest";
|
||||||
|
import attributeParser from "../src/public/app/services/attribute_parser.ts";
|
||||||
|
|
||||||
import { describe, it, expect, execute } from "./mini_test.js";
|
|
||||||
|
|
||||||
describe("Lexing", () => {
|
describe("Lexing", () => {
|
||||||
it("simple label", () => {
|
it("simple label", () => {
|
||||||
@ -40,7 +40,7 @@ describe("Lexing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Parser", () => {
|
describe.todo("Parser", () => {
|
||||||
it("simple label", () => {
|
it("simple label", () => {
|
||||||
const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));
|
const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));
|
||||||
|
|
||||||
@ -96,5 +96,3 @@ describe("error cases", () => {
|
|||||||
expect(() => attributeParser.lexAndParse("#")).toThrow(`Attribute name is empty, please fill the name.`);
|
expect(() => attributeParser.lexAndParse("#")).toThrow(`Attribute name is empty, please fill the name.`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
execute();
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { describe, it, execute, expect } from "./mini_test.ts";
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
import { getPlatformAppDataDir, getDataDirs} from "../src/services/data_dir.ts"
|
import { getPlatformAppDataDir, getDataDirs} from "../src/services/data_dir.ts"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe("data_dir.ts unit tests", () => {
|
describe("data_dir.ts unit tests", () => {
|
||||||
|
|
||||||
describe("#getPlatformAppDataDir()", () => {
|
describe("#getPlatformAppDataDir()", () => {
|
||||||
@ -65,7 +64,7 @@ describe("data_dir.ts unit tests", () => {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#getTriliumDataDir", () => {
|
describe.todo("#getTriliumDataDir", () => {
|
||||||
// TODO
|
// TODO
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -121,18 +120,37 @@ describe("data_dir.ts unit tests", () => {
|
|||||||
// make sure values are undefined
|
// make sure values are undefined
|
||||||
setMockedEnv(null);
|
setMockedEnv(null);
|
||||||
|
|
||||||
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
|
const mockDataDirBase = "/home/test/MOCK_TRILIUM_DATA_DIR"
|
||||||
const result = getDataDirs(mockDataDir);
|
const result = getDataDirs(mockDataDirBase);
|
||||||
|
|
||||||
|
// as per MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description
|
||||||
|
// Any attempt to change a frozen object will, either silently be ignored or
|
||||||
|
// throw a TypeError exception (most commonly, but not exclusively, when in strict mode).
|
||||||
|
// so be safe and check for both, even though it looks weird
|
||||||
|
|
||||||
|
const getChangeAttemptResult = () => {
|
||||||
|
try {
|
||||||
//@ts-expect-error - attempt to change value of readonly property
|
//@ts-expect-error - attempt to change value of readonly property
|
||||||
result.BACKUP_DIR = "attempt to change";
|
result.BACKUP_DIR = "attempt to change";
|
||||||
|
return result.BACKUP_DIR;
|
||||||
for (const key in result) {
|
|
||||||
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
|
|
||||||
}
|
}
|
||||||
|
catch(error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeAttemptResult = getChangeAttemptResult();
|
||||||
|
|
||||||
|
if (typeof changeAttemptResult === "string") {
|
||||||
|
// if it didn't throw above: assert that it did not change the value of it or any other keys of the object
|
||||||
|
for (const key in result) {
|
||||||
|
expect(result[key].startsWith(mockDataDirBase)).toBeTruthy()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expect(changeAttemptResult).toBeInstanceOf(TypeError)
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
execute()
|
|
@ -1,79 +0,0 @@
|
|||||||
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 "../src/services/sanitize_attribute_name";
|
import sanitizeAttributeName from "../src/services/sanitize_attribute_name";
|
||||||
import { describe, it, execute, expect } from "./mini_test";
|
|
||||||
|
|
||||||
// fn value, expected value
|
// fn value, expected value
|
||||||
const testCases: [fnValue: string, expectedValue: string][] = [
|
const testCases: [fnValue: string, expectedValue: string][] = [
|
||||||
@ -31,9 +31,7 @@ describe("sanitizeAttributeName unit tests", () => {
|
|||||||
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
|
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
|
||||||
const [value, expected] = testCase;
|
const [value, expected] = testCase;
|
||||||
const actual = sanitizeAttributeName(value);
|
const actual = sanitizeAttributeName(value);
|
||||||
expect(actual).toEqual(expected);
|
expect(actual).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
execute();
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import { expect, describe, it } from "vitest";
|
||||||
import { formatDownloadTitle } from "../../src/services/utils.ts";
|
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>][] = [
|
const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
|
||||||
// empty fileName tests
|
// empty fileName tests
|
||||||
@ -55,9 +55,7 @@ describe("utils/formatDownloadTitle unit tests", () => {
|
|||||||
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
|
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
|
||||||
const [value, expected] = testCase;
|
const [value, expected] = testCase;
|
||||||
const actual = formatDownloadTitle(...value);
|
const actual = formatDownloadTitle(...value);
|
||||||
expect(actual).toEqual(expected);
|
expect(actual).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
execute();
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
describe("Notes", () => {
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
|
describe.todo("Notes", () => {
|
||||||
it("zzz", () => {});
|
it("zzz", () => {});
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import lex from "../../src/services/search/services/lex.js";
|
import lex from "../../src/services/search/services/lex.js";
|
||||||
|
|
||||||
describe("Lexer fulltext", () => {
|
describe("Lexer fulltext", () => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import handleParens from "../../src/services/search/services/handle_parens.js";
|
import handleParens from "../../src/services/search/services/handle_parens.js";
|
||||||
import type { TokenStructure } from "../../src/services/search/services/types.js";
|
import type { TokenStructure } from "../../src/services/search/services/types.js";
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import AndExp from "../../src/services/search/expressions/and.js";
|
import AndExp from "../../src/services/search/expressions/and.js";
|
||||||
import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js";
|
import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js";
|
||||||
import type Expression from "../../src/services/search/expressions/expression.js";
|
import type Expression from "../../src/services/search/expressions/expression.js";
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, beforeEach, } from "vitest";
|
||||||
import searchService from "../../src/services/search/services/search.js";
|
import searchService from "../../src/services/search/services/search.js";
|
||||||
import BNote from "../../src/becca/entities/bnote.js";
|
import BNote from "../../src/becca/entities/bnote.js";
|
||||||
import BBranch from "../../src/becca/entities/bbranch.js";
|
import BBranch from "../../src/becca/entities/bbranch.js";
|
||||||
@ -21,7 +22,7 @@ describe("Search", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("simple path match", () => {
|
it.skip("simple path match", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -31,7 +32,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("normal search looks also at attributes", () => {
|
it.skip("normal search looks also at attributes", () => {
|
||||||
const austria = becca_mocking.note("Austria");
|
const austria = becca_mocking.note("Austria");
|
||||||
const vienna = becca_mocking.note("Vienna");
|
const vienna = becca_mocking.note("Vienna");
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("normal search looks also at type and mime", () => {
|
it.skip("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" }));
|
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();
|
const searchContext = new SearchContext();
|
||||||
@ -68,7 +69,7 @@ describe("Search", () => {
|
|||||||
expect(searchResults.length).toEqual(2);
|
expect(searchResults.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("only end leafs are results", () => {
|
it.skip("only end leafs are results", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -78,7 +79,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("only end leafs are results", () => {
|
it.skip("only end leafs are results", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")));
|
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -133,7 +134,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("inherited label comparison", () => {
|
it.skip("inherited label comparison", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic")));
|
rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -183,8 +184,7 @@ describe("Search", () => {
|
|||||||
|
|
||||||
function test(query: string, expectedResultCount: number) {
|
function test(query: string, expectedResultCount: number) {
|
||||||
const searchResults = searchService.findResultsWithQuery(query, searchContext);
|
const searchResults = searchService.findResultsWithQuery(query, searchContext);
|
||||||
expect(searchResults.length)
|
expect(searchResults.length, `While searching for ${query} got unexpected result: [${searchResults.join(", ")}]`)
|
||||||
.withContext(`While searching for ${query} got unexpected result: [${searchResults.join(", ")}]`)
|
|
||||||
.toEqual(expectedResultCount);
|
.toEqual(expectedResultCount);
|
||||||
|
|
||||||
if (expectedResultCount === 1) {
|
if (expectedResultCount === 1) {
|
||||||
@ -549,7 +549,7 @@ describe("Search", () => {
|
|||||||
expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
|
expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("test note.text *=* something", () => {
|
it.skip("test note.text *=* something", () => {
|
||||||
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
||||||
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
||||||
|
|
||||||
@ -562,7 +562,7 @@ describe("Search", () => {
|
|||||||
expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
|
expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("test that fulltext does not match archived notes", () => {
|
it.skip("test that fulltext does not match archived notes", () => {
|
||||||
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
||||||
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
import becca_mocking from "./becca_mocking.js";
|
import becca_mocking from "./becca_mocking.js";
|
||||||
import ValueExtractor from "../../src/services/search/value_extractor.js";
|
import ValueExtractor from "../../src/services/search/value_extractor.js";
|
||||||
import becca from "../../src/becca/becca.js";
|
import becca from "../../src/becca/becca.js";
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"spec_dir": "",
|
|
||||||
"spec_files": [
|
|
||||||
"spec/**/*.spec.ts",
|
|
||||||
"src/**/*.spec.ts"
|
|
||||||
],
|
|
||||||
"helpers": ["helpers/**/*.js"],
|
|
||||||
"stopSpecOnExpectationFailure": false,
|
|
||||||
"random": true
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import { trimIndentation } from "./utils.js";
|
import { trimIndentation } from "./utils.js";
|
||||||
|
|
||||||
describe("Utils", () => {
|
describe("Utils", () => {
|
||||||
|
@ -24,7 +24,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
|||||||
|
|
||||||
notePath?: string | null;
|
notePath?: string | null;
|
||||||
noteId?: string | null;
|
noteId?: string | null;
|
||||||
private parentNoteId?: string | null;
|
parentNoteId?: string | null;
|
||||||
viewScope?: ViewScope;
|
viewScope?: ViewScope;
|
||||||
|
|
||||||
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
||||||
|
@ -3,7 +3,7 @@ import contextMenu from "./context_menu.js";
|
|||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import type { ViewScope } from "../services/link.js";
|
import type { ViewScope } from "../services/link.js";
|
||||||
|
|
||||||
function openContextMenu(notePath: string, e: PointerEvent | JQuery.ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
function openContextMenu(notePath: string, e: PointerEvent | MouseEvent | JQuery.ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
||||||
contextMenu.show({
|
contextMenu.show({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
|
@ -68,22 +68,10 @@ const WHEEL_ZOOM: Library = {
|
|||||||
js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
|
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 = {
|
const MERMAID: Library = {
|
||||||
js: ["node_modules/mermaid/dist/mermaid.min.js"]
|
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 = {
|
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"]
|
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"]
|
||||||
};
|
};
|
||||||
@ -209,9 +197,7 @@ export default {
|
|||||||
CALENDAR_WIDGET,
|
CALENDAR_WIDGET,
|
||||||
KATEX,
|
KATEX,
|
||||||
WHEEL_ZOOM,
|
WHEEL_ZOOM,
|
||||||
FORCE_GRAPH,
|
|
||||||
MERMAID,
|
MERMAID,
|
||||||
MERMAID_ELK,
|
|
||||||
EXCALIDRAW,
|
EXCALIDRAW,
|
||||||
MARKJS,
|
MARKJS,
|
||||||
I18NEXT,
|
I18NEXT,
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import library_loader from "./library_loader.js";
|
|
||||||
|
|
||||||
let elkLoaded = false;
|
let elkLoaded = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,7 +20,6 @@ export async function loadElkIfNeeded(mermaidContent: string) {
|
|||||||
});
|
});
|
||||||
if (parsedContent?.config?.layout === "elk") {
|
if (parsedContent?.config?.layout === "elk") {
|
||||||
elkLoaded = true;
|
elkLoaded = true;
|
||||||
await library_loader.requireLibrary(library_loader.MERMAID_ELK);
|
mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default);
|
||||||
mermaid.registerLayoutLoaders(MERMAID_ELK);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
src/public/app/types.d.ts
vendored
5
src/public/app/types.d.ts
vendored
@ -155,13 +155,12 @@ declare global {
|
|||||||
registerLayoutLoaders(loader: MermaidLoader);
|
registerLayoutLoaders(loader: MermaidLoader);
|
||||||
parse(content: string, opts: {
|
parse(content: string, opts: {
|
||||||
suppressErrors: true
|
suppressErrors: true
|
||||||
}): {
|
}): Promise<{
|
||||||
config: {
|
config: {
|
||||||
layout: string;
|
layout: string;
|
||||||
}
|
}
|
||||||
}
|
}>
|
||||||
};
|
};
|
||||||
var MERMAID_ELK: MermaidLoader;
|
|
||||||
|
|
||||||
var CKEditor: {
|
var CKEditor: {
|
||||||
BalloonEditor: {
|
BalloonEditor: {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import libraryLoader from "../services/library_loader.js";
|
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import attributeService from "../services/attributes.js";
|
import attributeService from "../services/attributes.js";
|
||||||
import hoistedNoteService from "../services/hoisted_note.js";
|
import hoistedNoteService from "../services/hoisted_note.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext, { type EventData } from "../components/app_context.js";
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||||
import linkContextMenuService from "../menus/link_context_menu.js";
|
import linkContextMenuService from "../menus/link_context_menu.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
|
import type ForceGraph from "force-graph";
|
||||||
|
import type { GraphData, LinkObject, NodeObject } from "force-graph";
|
||||||
|
import type FNote from "../entities/fnote.js";
|
||||||
|
|
||||||
const esc = utils.escapeHtml;
|
const esc = utils.escapeHtml;
|
||||||
|
|
||||||
@ -93,8 +95,80 @@ const TPL = `<div class="note-map-widget" style="position: relative;">
|
|||||||
<div class="note-map-container"></div>
|
<div class="note-map-container"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
type WidgetMode = "type" | "ribbon";
|
||||||
|
type MapType = "tree" | "link";
|
||||||
|
type Data = GraphData<NodeObject, LinkObject<NodeObject>>;
|
||||||
|
|
||||||
|
interface Node extends NodeObject {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Link extends LinkObject<NodeObject> {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
source: Node;
|
||||||
|
target: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotesAndRelationsData {
|
||||||
|
nodes: Node[];
|
||||||
|
links: {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
name: string;
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace
|
||||||
|
interface ResponseLink {
|
||||||
|
key: string;
|
||||||
|
sourceNoteId: string;
|
||||||
|
targetNoteId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostNotesMapResponse {
|
||||||
|
notes: string[];
|
||||||
|
links: ResponseLink[],
|
||||||
|
noteIdToDescendantCountMap: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedLink {
|
||||||
|
id: string;
|
||||||
|
sourceNoteId: string;
|
||||||
|
targetNoteId: string;
|
||||||
|
names: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CssData {
|
||||||
|
fontFamily: string;
|
||||||
|
textColor: string;
|
||||||
|
mutedTextColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class NoteMapWidget extends NoteContextAwareWidget {
|
export default class NoteMapWidget extends NoteContextAwareWidget {
|
||||||
constructor(widgetMode) {
|
|
||||||
|
private fixNodes: boolean;
|
||||||
|
private widgetMode: WidgetMode;
|
||||||
|
private mapType?: MapType;
|
||||||
|
private cssData!: CssData;
|
||||||
|
|
||||||
|
private themeStyle!: string;
|
||||||
|
private $container!: JQuery<HTMLElement>;
|
||||||
|
private $styleResolver!: JQuery<HTMLElement>;
|
||||||
|
private graph!: ForceGraph;
|
||||||
|
private noteIdToSizeMap!: Record<string, number>;
|
||||||
|
private zoomLevel!: number;
|
||||||
|
private nodes!: Node[];
|
||||||
|
|
||||||
|
constructor(widgetMode: WidgetMode) {
|
||||||
super();
|
super();
|
||||||
this.fixNodes = false; // needed to save the status of the UI element. Is set later in the code
|
this.fixNodes = false; // needed to save the status of the UI element. Is set later in the code
|
||||||
this.widgetMode = widgetMode; // 'type' or 'ribbon'
|
this.widgetMode = widgetMode; // 'type' or 'ribbon'
|
||||||
@ -114,7 +188,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
this.$widget.find(".map-type-switcher button").on("click", async (e) => {
|
this.$widget.find(".map-type-switcher button").on("click", async (e) => {
|
||||||
const type = $(e.target).closest("button").attr("data-type");
|
const type = $(e.target).closest("button").attr("data-type");
|
||||||
|
|
||||||
await attributeService.setLabel(this.noteId, "mapType", type);
|
await attributeService.setLabel(this.noteId ?? "", "mapType", type);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reading the status of the Drag nodes Ui element. Changing it´s color when activated. Reading Force value of the link distance.
|
// Reading the status of the Drag nodes Ui element. Changing it´s color when activated. Reading Force value of the link distance.
|
||||||
@ -135,31 +209,32 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
const $parent = this.$widget.parent();
|
const $parent = this.$widget.parent();
|
||||||
|
|
||||||
this.graph.height($parent.height()).width($parent.width());
|
this.graph
|
||||||
|
.height($parent.height() || 0)
|
||||||
|
.width($parent.width() || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWithNote(note) {
|
async refreshWithNote(note: FNote) {
|
||||||
this.$widget.show();
|
this.$widget.show();
|
||||||
|
|
||||||
this.css = {
|
this.cssData = {
|
||||||
fontFamily: this.$container.css("font-family"),
|
fontFamily: this.$container.css("font-family"),
|
||||||
textColor: this.rgb2hex(this.$container.css("color")),
|
textColor: this.rgb2hex(this.$container.css("color")),
|
||||||
mutedTextColor: this.rgb2hex(this.$styleResolver.css("color"))
|
mutedTextColor: this.rgb2hex(this.$styleResolver.css("color"))
|
||||||
};
|
};
|
||||||
|
|
||||||
this.mapType = this.note.getLabelValue("mapType") === "tree" ? "tree" : "link";
|
this.mapType = note.getLabelValue("mapType") === "tree" ? "tree" : "link";
|
||||||
|
|
||||||
await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH);
|
|
||||||
|
|
||||||
//variables for the hover effekt. We have to save the neighbours of a hovered node in a set. Also we need to save the links as well as the hovered node itself
|
//variables for the hover effekt. We have to save the neighbours of a hovered node in a set. Also we need to save the links as well as the hovered node itself
|
||||||
|
|
||||||
let hoverNode = null;
|
let hoverNode: NodeObject | null = null;
|
||||||
const highlightLinks = new Set();
|
const highlightLinks = new Set();
|
||||||
const neighbours = new Set();
|
const neighbours = new Set();
|
||||||
|
|
||||||
this.graph = ForceGraph()(this.$container[0])
|
const ForceGraph = (await import("force-graph")).default;
|
||||||
.width(this.$container.width())
|
this.graph = new ForceGraph(this.$container[0])
|
||||||
.height(this.$container.height())
|
.width(this.$container.width() || 0)
|
||||||
|
.height(this.$container.height() || 0)
|
||||||
.onZoom((zoom) => this.setZoomLevel(zoom.k))
|
.onZoom((zoom) => this.setZoomLevel(zoom.k))
|
||||||
.d3AlphaDecay(0.01)
|
.d3AlphaDecay(0.01)
|
||||||
.d3VelocityDecay(0.08)
|
.d3VelocityDecay(0.08)
|
||||||
@ -170,8 +245,8 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
node.fx = node.x;
|
node.fx = node.x;
|
||||||
node.fy = node.y;
|
node.fy = node.y;
|
||||||
} else {
|
} else {
|
||||||
node.fx = null;
|
node.fx = undefined;
|
||||||
node.fy = null;
|
node.fy = undefined;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//check if hovered and set the hovernode variable, saving the hovered node object into it. Clear links variable everytime you hover. Without clearing links will stay highlighted
|
//check if hovered and set the hovernode variable, saving the hovered node object into it. Clear links variable everytime you hover. Without clearing links will stay highlighted
|
||||||
@ -182,17 +257,19 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
// set link width to immitate a highlight effekt. Checking the condition if any links are saved in the previous defined set highlightlinks
|
// set link width to immitate a highlight effekt. Checking the condition if any links are saved in the previous defined set highlightlinks
|
||||||
.linkWidth((link) => (highlightLinks.has(link) ? 3 : 0.4))
|
.linkWidth((link) => (highlightLinks.has(link) ? 3 : 0.4))
|
||||||
.linkColor((link) => (highlightLinks.has(link) ? "white" : this.css.mutedTextColor))
|
.linkColor((link) => (highlightLinks.has(link) ? "white" : this.cssData.mutedTextColor))
|
||||||
.linkDirectionalArrowLength(4)
|
.linkDirectionalArrowLength(4)
|
||||||
.linkDirectionalArrowRelPos(0.95)
|
.linkDirectionalArrowRelPos(0.95)
|
||||||
|
|
||||||
// main code for highlighting hovered nodes and neighbours. here we "style" the nodes. the nodes are rendered several hundred times per second.
|
// main code for highlighting hovered nodes and neighbours. here we "style" the nodes. the nodes are rendered several hundred times per second.
|
||||||
.nodeCanvasObject((node, ctx) => {
|
.nodeCanvasObject((_node, ctx) => {
|
||||||
|
const node = _node as Node;
|
||||||
if (hoverNode == node) {
|
if (hoverNode == node) {
|
||||||
//paint only hovered node
|
//paint only hovered node
|
||||||
this.paintNode(node, "#661822", ctx);
|
this.paintNode(node, "#661822", ctx);
|
||||||
neighbours.clear(); //clearing neighbours or the effect would be maintained after hovering is over
|
neighbours.clear(); //clearing neighbours or the effect would be maintained after hovering is over
|
||||||
for (const link of data.links) {
|
for (const _link of data.links) {
|
||||||
|
const link = _link as unknown as Link;
|
||||||
//check if node is part of a link in the canvas, if so add it´s neighbours and related links to the previous defined variables to paint the nodes
|
//check if node is part of a link in the canvas, if so add it´s neighbours and related links to the previous defined variables to paint the nodes
|
||||||
if (link.source.id == node.id || link.target.id == node.id) {
|
if (link.source.id == node.id || link.target.id == node.id) {
|
||||||
neighbours.add(link.source);
|
neighbours.add(link.source);
|
||||||
@ -209,23 +286,39 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.getColorForNode(node), ctx))
|
.nodePointerAreaPaint((node, _, ctx) => this.paintNode(node as Node, this.getColorForNode(node as Node), ctx))
|
||||||
.nodePointerAreaPaint((node, color, ctx) => {
|
.nodePointerAreaPaint((node, color, ctx) => {
|
||||||
|
if (!node.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(node.x, node.y, this.noteIdToSizeMap[node.id], 0, 2 * Math.PI, false);
|
if (node.x && node.y) {
|
||||||
|
ctx.arc(node.x, node.y,
|
||||||
|
this.noteIdToSizeMap[node.id], 0,
|
||||||
|
2 * Math.PI, false);
|
||||||
|
}
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
})
|
})
|
||||||
.nodeLabel((node) => esc(node.name))
|
.nodeLabel((node) => esc((node as Node).name))
|
||||||
.maxZoom(7)
|
.maxZoom(7)
|
||||||
.warmupTicks(30)
|
.warmupTicks(30)
|
||||||
.onNodeClick((node) => appContext.tabManager.getActiveContext().setNote(node.id))
|
.onNodeClick((node) => {
|
||||||
.onNodeRightClick((node, e) => linkContextMenuService.openContextMenu(node.id, e));
|
if (node.id) {
|
||||||
|
appContext.tabManager.getActiveContext().setNote((node as Node).id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onNodeRightClick((node, e) => {
|
||||||
|
if (node.id) {
|
||||||
|
linkContextMenuService.openContextMenu((node as Node).id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (this.mapType === "link") {
|
if (this.mapType === "link") {
|
||||||
this.graph
|
this.graph
|
||||||
.linkLabel((l) => `${esc(l.source.name)} - <strong>${esc(l.name)}</strong> - ${esc(l.target.name)}`)
|
.linkLabel((l) => `${esc((l as Link).source.name)} - <strong>${esc((l as Link).name)}</strong> - ${esc((l as Link).target.name)}`)
|
||||||
.linkCanvasObject((link, ctx) => this.paintLink(link, ctx))
|
.linkCanvasObject((link, ctx) => this.paintLink(link as Link, ctx))
|
||||||
.linkCanvasObjectMode(() => "after");
|
.linkCanvasObjectMode(() => "after");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,25 +332,25 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
let distancevalue = 40; // default value for the link force of the nodes
|
let distancevalue = 40; // default value for the link force of the nodes
|
||||||
|
|
||||||
this.$widget.find(".fixnodes-type-switcher input").on("change", async (e) => {
|
this.$widget.find(".fixnodes-type-switcher input").on("change", async (e) => {
|
||||||
distancevalue = e.target.closest("input").value;
|
distancevalue = parseInt(e.target.closest("input")?.value ?? "0");
|
||||||
this.graph.d3Force("link").distance(distancevalue);
|
this.graph.d3Force("link")?.distance(distancevalue);
|
||||||
|
|
||||||
this.renderData(data);
|
this.renderData(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.graph.d3Force("center").strength(0.2);
|
this.graph.d3Force("center")?.strength(0.2);
|
||||||
this.graph.d3Force("charge").strength(boundedCharge);
|
this.graph.d3Force("charge")?.strength(boundedCharge);
|
||||||
this.graph.d3Force("charge").distanceMax(1000);
|
this.graph.d3Force("charge")?.distanceMax(1000);
|
||||||
|
|
||||||
this.renderData(data);
|
this.renderData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMapRootNoteId() {
|
getMapRootNoteId(): string {
|
||||||
if (this.widgetMode === "ribbon") {
|
if (this.noteId && this.widgetMode === "ribbon") {
|
||||||
return this.noteId;
|
return this.noteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mapRootNoteId = this.note.getLabelValue("mapRootNoteId");
|
let mapRootNoteId = this.note?.getLabelValue("mapRootNoteId");
|
||||||
|
|
||||||
if (mapRootNoteId === "hoisted") {
|
if (mapRootNoteId === "hoisted") {
|
||||||
mapRootNoteId = hoistedNoteService.getHoistedNoteId();
|
mapRootNoteId = hoistedNoteService.getHoistedNoteId();
|
||||||
@ -265,10 +358,10 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
mapRootNoteId = appContext.tabManager.getActiveContext().parentNoteId;
|
mapRootNoteId = appContext.tabManager.getActiveContext().parentNoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapRootNoteId;
|
return mapRootNoteId ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getColorForNode(node) {
|
getColorForNode(node: Node) {
|
||||||
if (node.color) {
|
if (node.color) {
|
||||||
return node.color;
|
return node.color;
|
||||||
} else if (this.widgetMode === "ribbon" && node.id === this.noteId) {
|
} else if (this.widgetMode === "ribbon" && node.id === this.noteId) {
|
||||||
@ -278,7 +371,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateColorFromString(str) {
|
generateColorFromString(str: string) {
|
||||||
if (this.themeStyle === "dark") {
|
if (this.themeStyle === "dark") {
|
||||||
str = `0${str}`; // magic lightning modifier
|
str = `0${str}`; // magic lightning modifier
|
||||||
}
|
}
|
||||||
@ -297,20 +390,22 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
rgb2hex(rgb) {
|
rgb2hex(rgb: string) {
|
||||||
return `#${rgb
|
return `#${(rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) || [])
|
||||||
.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
|
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
|
.map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
|
||||||
.join("")}`;
|
.join("")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setZoomLevel(level) {
|
setZoomLevel(level: number) {
|
||||||
this.zoomLevel = level;
|
this.zoomLevel = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
paintNode(node, color, ctx) {
|
paintNode(node: Node, color: string, ctx: CanvasRenderingContext2D) {
|
||||||
const { x, y } = node;
|
const { x, y } = node;
|
||||||
|
if (!x || !y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const size = this.noteIdToSizeMap[node.id];
|
const size = this.noteIdToSizeMap[node.id];
|
||||||
|
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
@ -324,8 +419,8 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = this.css.textColor;
|
ctx.fillStyle = this.cssData.textColor;
|
||||||
ctx.font = `${size}px ${this.css.fontFamily}`;
|
ctx.font = `${size}px ${this.cssData.fontFamily}`;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
|
|
||||||
@ -338,26 +433,29 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
ctx.fillText(title, x, y + Math.round(size * 1.5));
|
ctx.fillText(title, x, y + Math.round(size * 1.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
paintLink(link, ctx) {
|
paintLink(link: Link, ctx: CanvasRenderingContext2D) {
|
||||||
if (this.zoomLevel < 5) {
|
if (this.zoomLevel < 5) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.font = `3px ${this.css.fontFamily}`;
|
ctx.font = `3px ${this.cssData.fontFamily}`;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.fillStyle = this.css.mutedTextColor;
|
ctx.fillStyle = this.cssData.mutedTextColor;
|
||||||
|
|
||||||
const { source, target } = link;
|
const { source, target } = link;
|
||||||
|
if (typeof source !== "object" || typeof target !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const x = (source.x + target.x) / 2;
|
if (source.x && source.y && target.x && target.y) {
|
||||||
const y = (source.y + target.y) / 2;
|
const x = ((source.x) + (target.x)) / 2;
|
||||||
|
const y = ((source.y) + (target.y)) / 2;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(x, y);
|
ctx.translate(x, y);
|
||||||
|
|
||||||
const deltaY = source.y - target.y;
|
const deltaY = (source.y) - (target.y);
|
||||||
const deltaX = source.x - target.x;
|
const deltaX = (source.x) - (target.x);
|
||||||
|
|
||||||
let angle = Math.atan2(deltaY, deltaX);
|
let angle = Math.atan2(deltaY, deltaX);
|
||||||
let moveY = 2;
|
let moveY = 2;
|
||||||
@ -369,11 +467,13 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
ctx.rotate(angle);
|
ctx.rotate(angle);
|
||||||
ctx.fillText(link.name, 0, moveY);
|
ctx.fillText(link.name, 0, moveY);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadNotesAndRelations(mapRootNoteId) {
|
async loadNotesAndRelations(mapRootNoteId: string): Promise<NotesAndRelationsData> {
|
||||||
const resp = await server.post(`note-map/${mapRootNoteId}/${this.mapType}`);
|
const resp = await server.post<PostNotesMapResponse>(`note-map/${mapRootNoteId}/${this.mapType}`);
|
||||||
|
|
||||||
this.calculateNodeSizes(resp);
|
this.calculateNodeSizes(resp);
|
||||||
|
|
||||||
@ -397,8 +497,8 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroupedLinks(links) {
|
getGroupedLinks(links: ResponseLink[]): GroupedLink[] {
|
||||||
const linksGroupedBySourceTarget = {};
|
const linksGroupedBySourceTarget: Record<string, GroupedLink> = {};
|
||||||
|
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
const key = `${link.sourceNoteId}-${link.targetNoteId}`;
|
const key = `${link.sourceNoteId}-${link.targetNoteId}`;
|
||||||
@ -420,7 +520,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
return Object.values(linksGroupedBySourceTarget);
|
return Object.values(linksGroupedBySourceTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateNodeSizes(resp) {
|
calculateNodeSizes(resp: PostNotesMapResponse) {
|
||||||
this.noteIdToSizeMap = {};
|
this.noteIdToSizeMap = {};
|
||||||
|
|
||||||
if (this.mapType === "tree") {
|
if (this.mapType === "tree") {
|
||||||
@ -436,7 +536,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.mapType === "link") {
|
} else if (this.mapType === "link") {
|
||||||
const noteIdToLinkCount = {};
|
const noteIdToLinkCount: Record<string, number> = {};
|
||||||
|
|
||||||
for (const link of resp.links) {
|
for (const link of resp.links) {
|
||||||
noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0);
|
noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0);
|
||||||
@ -452,7 +552,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderData(data) {
|
renderData(data: Data) {
|
||||||
this.graph.graphData(data);
|
this.graph.graphData(data);
|
||||||
|
|
||||||
if (this.widgetMode === "ribbon" && this.note?.type !== "search") {
|
if (this.widgetMode === "ribbon" && this.note?.type !== "search") {
|
||||||
@ -475,7 +575,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
const noteIdsWithLinks = this.getNoteIdsWithLinks(data);
|
const noteIdsWithLinks = this.getNoteIdsWithLinks(data);
|
||||||
|
|
||||||
if (noteIdsWithLinks.size > 0) {
|
if (noteIdsWithLinks.size > 0) {
|
||||||
this.graph.zoomToFit(400, 30, (node) => noteIdsWithLinks.has(node.id));
|
this.graph.zoomToFit(400, 30, (node) => noteIdsWithLinks.has(node.id ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noteIdsWithLinks.size < 30) {
|
if (noteIdsWithLinks.size < 30) {
|
||||||
@ -486,26 +586,36 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNoteIdsWithLinks(data) {
|
getNoteIdsWithLinks(data: Data) {
|
||||||
const noteIds = new Set();
|
const noteIds = new Set<string | number>();
|
||||||
|
|
||||||
for (const link of data.links) {
|
for (const link of data.links) {
|
||||||
|
if (typeof link.source === "object" && link.source.id) {
|
||||||
noteIds.add(link.source.id);
|
noteIds.add(link.source.id);
|
||||||
|
}
|
||||||
|
if (typeof link.target === "object" && link.target.id) {
|
||||||
noteIds.add(link.target.id);
|
noteIds.add(link.target.id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return noteIds;
|
return noteIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubGraphConnectedToCurrentNote(data) {
|
getSubGraphConnectedToCurrentNote(data: Data) {
|
||||||
function getGroupedLinks(links, type) {
|
function getGroupedLinks(links: LinkObject<NodeObject>[], type: "source" | "target") {
|
||||||
const map = {};
|
const map: Record<string | number, LinkObject<NodeObject>[]> = {};
|
||||||
|
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
|
if (typeof link[type] !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const key = link[type].id;
|
const key = link[type].id;
|
||||||
|
if (key) {
|
||||||
map[key] = map[key] || [];
|
map[key] = map[key] || [];
|
||||||
map[key].push(link);
|
map[key].push(link);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@ -515,19 +625,23 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
const subGraphNoteIds = new Set();
|
const subGraphNoteIds = new Set();
|
||||||
|
|
||||||
function traverseGraph(noteId) {
|
function traverseGraph(noteId?: string | number) {
|
||||||
if (subGraphNoteIds.has(noteId)) {
|
if (!noteId || subGraphNoteIds.has(noteId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
subGraphNoteIds.add(noteId);
|
subGraphNoteIds.add(noteId);
|
||||||
|
|
||||||
for (const link of linksBySource[noteId] || []) {
|
for (const link of linksBySource[noteId] || []) {
|
||||||
traverseGraph(link.target.id);
|
if (typeof link.target === "object") {
|
||||||
|
traverseGraph(link.target?.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const link of linksByTarget[noteId] || []) {
|
for (const link of linksByTarget[noteId] || []) {
|
||||||
traverseGraph(link.source.id);
|
if (typeof link.source === "object") {
|
||||||
|
traverseGraph(link.source?.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -539,8 +653,9 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
|
|||||||
this.$container.html("");
|
this.$container.html("");
|
||||||
}
|
}
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }) {
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
if (loadResults.getAttributeRows(this.componentId).find((attr) => attr.type === "label" && ["mapType", "mapRootNoteId"].includes(attr.name) && attributeService.isAffecting(attr, this.note))) {
|
if (loadResults.getAttributeRows(this.componentId)
|
||||||
|
.find((attr) => attr.type === "label" && ["mapType", "mapRootNoteId"].includes(attr.name || "") && attributeService.isAffecting(attr, this.note))) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -63,7 +63,6 @@ async function register(app: express.Application) {
|
|||||||
app.use(`/${assetPath}/node_modules/katex/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/katex/dist/")));
|
app.use(`/${assetPath}/node_modules/katex/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/katex/dist/")));
|
||||||
|
|
||||||
app.use(`/${assetPath}/node_modules/dayjs/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/dayjs/")));
|
app.use(`/${assetPath}/node_modules/dayjs/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/dayjs/")));
|
||||||
app.use(`/${assetPath}/node_modules/force-graph/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/force-graph/dist/")));
|
|
||||||
|
|
||||||
app.use(`/${assetPath}/node_modules/boxicons/css/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/boxicons/css/")));
|
app.use(`/${assetPath}/node_modules/boxicons/css/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/boxicons/css/")));
|
||||||
app.use(`/${assetPath}/node_modules/boxicons/fonts/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/boxicons/fonts/")));
|
app.use(`/${assetPath}/node_modules/boxicons/fonts/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/boxicons/fonts/")));
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { doubleCsrf } from "csrf-csrf";
|
import { doubleCsrf } from "csrf-csrf";
|
||||||
import sessionSecret from "../services/session_secret.js";
|
import sessionSecret from "../services/session_secret.js";
|
||||||
|
import { isElectron } from "../services/utils.js";
|
||||||
|
|
||||||
const doubleCsrfUtilities = doubleCsrf({
|
const doubleCsrfUtilities = doubleCsrf({
|
||||||
getSecret: () => sessionSecret,
|
getSecret: () => sessionSecret,
|
||||||
@ -7,7 +8,7 @@ const doubleCsrfUtilities = doubleCsrf({
|
|||||||
path: "", // empty, so cookie is valid only for the current path
|
path: "", // empty, so cookie is valid only for the current path
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
httpOnly: true
|
httpOnly: !isElectron() // set to false for Electron, see https://github.com/TriliumNext/Notes/pull/966
|
||||||
},
|
},
|
||||||
cookieName: "_csrf"
|
cookieName: "_csrf"
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import markdownExportService from "./md.js";
|
import markdownExportService from "./md.js";
|
||||||
import { trimIndentation } from "../../../spec/support/utils.js";
|
import { trimIndentation } from "../../../spec/support/utils.js";
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import { trimIndentation } from "../../../spec/support/utils.js";
|
import { trimIndentation } from "../../../spec/support/utils.js";
|
||||||
import markdownService from "./markdown.js";
|
import markdownService from "./markdown.js";
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import { renderCode, type Result } from "./content_renderer.js";
|
import { renderCode, type Result } from "./content_renderer.js";
|
||||||
|
|
||||||
describe("content_renderer", () => {
|
describe("content_renderer", () => {
|
||||||
@ -8,7 +9,7 @@ describe("content_renderer", () => {
|
|||||||
content: " "
|
content: " "
|
||||||
};
|
};
|
||||||
renderCode(emptyResult);
|
renderCode(emptyResult);
|
||||||
expect(emptyResult.isEmpty).toBeTrue();
|
expect(emptyResult.isEmpty).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("identifies unsupported content type", () => {
|
it("identifies unsupported content type", () => {
|
||||||
@ -17,7 +18,7 @@ describe("content_renderer", () => {
|
|||||||
content: Buffer.from("Hello world")
|
content: Buffer.from("Hello world")
|
||||||
};
|
};
|
||||||
renderCode(emptyResult);
|
renderCode(emptyResult);
|
||||||
expect(emptyResult.isEmpty).toBeTrue();
|
expect(emptyResult.isEmpty).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("wraps code in <pre>", () => {
|
it("wraps code in <pre>", () => {
|
||||||
|
24
vitest.config.ts
Normal file
24
vitest.config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import { configDefaults, coverageConfigDefaults } from "vitest/config";
|
||||||
|
|
||||||
|
const customExcludes = [
|
||||||
|
"build/**",
|
||||||
|
"e2e/**",
|
||||||
|
"integration-tests/**",
|
||||||
|
"tests-examples/**",
|
||||||
|
"node_modules/**",
|
||||||
|
"src/public/app-dist/**",
|
||||||
|
"libraries/**",
|
||||||
|
"docs/**",
|
||||||
|
"out/**",
|
||||||
|
"*.config.[jt]s" // playwright.config.ts and similar
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
exclude: [...configDefaults.exclude, ...customExcludes],
|
||||||
|
coverage: {
|
||||||
|
exclude: [...coverageConfigDefaults.exclude, ...customExcludes]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user