Merge branch 'develop' into renovate/katex-0.x

This commit is contained in:
Elian Doran 2025-01-17 22:22:53 +02:00 committed by GitHub
commit fec0ae62b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
179 changed files with 2669 additions and 2582 deletions

View File

@ -8,5 +8,10 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.json] [{server,translation}.json]
charset = utf-8
end_of_line = lf
indent_size = 2 indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

1
.gitattributes vendored
View File

@ -1 +1,2 @@
package-lock.json linguist-generated=true
**/package-lock.json linguist-generated=true **/package-lock.json linguist-generated=true

View File

@ -16,9 +16,28 @@ env:
TEST_TAG: ${{ github.repository_owner }}/notes:test TEST_TAG: ${{ github.repository_owner }}/notes:test
jobs: jobs:
test_dev:
name: Test development
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Run the TypeScript build
run: npx tsc
build_docker: build_docker:
name: Build Docker image name: Build Docker image
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- test_dev
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up node & dependencies - name: Set up node & dependencies
@ -37,26 +56,11 @@ jobs:
context: . context: .
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
test_dev:
name: Test development
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Run the TypeScript build
run: npx tsc
test_docker: test_docker:
name: Check Docker build name: Check Docker build
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- build_docker
strategy: strategy:
matrix: matrix:
include: include:

View File

@ -48,7 +48,11 @@ jobs:
node-version: 20 node-version: 20
cache: "npm" cache: "npm"
- run: npm ci - name: Install npm dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run the TypeScript build - name: Run the TypeScript build
run: npx tsc run: npx tsc
@ -68,7 +72,7 @@ jobs:
- name: Validate container run output - name: Validate container run output
run: | run: |
CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host --name trilium_local ${{ env.TEST_TAG }}) CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }})
echo "Container ID: $CONTAINER_ID" echo "Container ID: $CONTAINER_ID"
- name: Wait for the healthchecks to pass - name: Wait for the healthchecks to pass
@ -79,14 +83,12 @@ jobs:
require-status: running require-status: running
require-healthy: true require-healthy: true
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests - name: Run Playwright tests
run: TRILIUM_DOCKER=1 npx playwright test run: TRILIUM_DOCKER=1 npx playwright test
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
name: playwright-report name: Playwright report (${{ matrix.dockerfile }})
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -464,9 +464,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/better-sqlite3": { "node_modules/better-sqlite3": {
"version": "11.7.2", "version": "11.8.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.0.tgz",
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==", "integrity": "sha512-aKv9s2dir7bsEX5RIjL9HHWB9uQ+f6Vch5B4qmeAOop4Y9OYHX+PNKLr+mpv6+d8L/ZYh4l7H8zPuVMbWkVMLw==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
@ -1516,9 +1516,9 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
}, },
"better-sqlite3": { "better-sqlite3": {
"version": "11.7.2", "version": "11.8.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.0.tgz",
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==", "integrity": "sha512-aKv9s2dir7bsEX5RIjL9HHWB9uQ+f6Vch5B4qmeAOop4Y9OYHX+PNKLr+mpv6+d8L/ZYh4l7H8zPuVMbWkVMLw==",
"requires": { "requires": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.1"

View File

@ -1,6 +1,12 @@
import { test, expect, Page } from "@playwright/test"; import { test, expect, Page } from "@playwright/test";
import App from "./support/app"; import App from "./support/app";
test.afterEach(async ({ page, context }) => {
const app = new App(page, context);
// Ensure English is set after each locale change to avoid any leaks to other tests.
await app.setOption("locale", "en");
});
test("Displays translation on desktop", async ({ page, context }) => { test("Displays translation on desktop", async ({ page, context }) => {
const app = new App(page, context); const app = new App(page, context);
await app.goto(); await app.goto();
@ -43,9 +49,9 @@ test("User can change language from settings", async ({ page, context }) => {
// Select Chinese and ensure the translation is set. // Select Chinese and ensure the translation is set.
await languageCombobox.selectOption("cn"); await languageCombobox.selectOption("cn");
await expect(app.currentNoteSplit).toContainText("主题"); await expect(app.currentNoteSplit).toContainText("主题", { timeout: 15000 });
// Select English again. // Select English again.
await languageCombobox.selectOption("en"); await languageCombobox.selectOption("en");
await expect(app.currentNoteSplit).toContainText("Language"); await expect(app.currentNoteSplit).toContainText("Language", { timeout: 15000 });
}); });

View File

@ -10,7 +10,7 @@ test("Displays lint warnings for backend script", async ({ page, context }) => {
const codeEditor = app.currentNoteSplit.locator(".CodeMirror"); const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
// Expect two warning signs in the gutter. // Expect two warning signs in the gutter.
expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2); await expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2);
// Hover over hello // Hover over hello
await codeEditor.getByText("hello").first().hover(); await codeEditor.getByText("hello").first().hover();

View File

@ -1,22 +1,67 @@
import { test, expect, Page } from "@playwright/test"; import { test, expect, Page, BrowserContext } from "@playwright/test";
import App from "../support/app"; import App from "../support/app";
test("displays simple map", async ({ page, context }) => { test("renders ELK flowchart", async ({ page, context }) => {
const app = new App(page, context); await testAriaSnapshot({
await app.goto(); page, context,
await app.goToNoteInNewTab("Sample mindmap"); noteTitle: "Flowchart ELK on",
snapshot: `
expect(app.currentNoteSplit).toContainText("Hello world"); - document:
expect(app.currentNoteSplit).toContainText("1"); - paragraph: A
expect(app.currentNoteSplit).toContainText("1a"); - 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("displays note settings", async ({ page, context }) => { 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); const app = new App(page, context);
await app.goto(); await app.goto();
await app.goToNoteInNewTab("Sample mindmap"); await app.goToNoteInNewTab(noteTitle);
await app.currentNoteSplit.getByText("Hello world").click({ force: true }); const svgData = app.currentNoteSplit.locator(".mermaid-render svg");
const nodeMenu = app.currentNoteSplit.locator(".node-menu"); await expect(svgData).toBeVisible();
expect(nodeMenu).toBeVisible(); await expect(svgData).toMatchAriaSnapshot(snapshot);
}); }

View File

@ -0,0 +1,22 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
test("displays simple map", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Sample mindmap");
await expect(app.currentNoteSplit).toContainText("Hello world");
await expect(app.currentNoteSplit).toContainText("1");
await expect(app.currentNoteSplit).toContainText("1a");
});
test("displays note settings", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Sample mindmap");
await app.currentNoteSplit.getByText("Hello world").click({ force: true });
const nodeMenu = app.currentNoteSplit.locator(".node-menu");
await expect(nodeMenu).toBeVisible();
});

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

View File

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

View File

@ -5,12 +5,15 @@ interface GotoOpts {
isMobile?: boolean; isMobile?: boolean;
} }
const BASE_URL = "http://127.0.0.1:8082";
export default class App { export default class App {
readonly page: Page; readonly page: Page;
readonly context: BrowserContext; readonly context: BrowserContext;
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;
@ -20,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");
} }
@ -27,7 +31,7 @@ export default class App {
async goto(opts: GotoOpts = {}) { async goto(opts: GotoOpts = {}) {
await this.context.addCookies([ await this.context.addCookies([
{ {
url: "http://127.0.0.1:8082", url: BASE_URL,
name: "trilium-device", name: "trilium-device",
value: opts.isMobile ? "mobile" : "desktop" value: opts.isMobile ? "mobile" : "desktop"
} }
@ -60,19 +64,49 @@ export default class App {
return this.tabBar.locator(".note-tab[active]"); return this.tabBar.locator(".note-tab[active]");
} }
/**
* Closes all the tabs in the client by issuing a command.
*/
async closeAllTabs() { async closeAllTabs() {
await this.getTab(0).click({ button: "right" }); await this.triggerCommand("closeAllTabs");
await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise
await this.page.getByText("Close all tabs").click({ force: true });
await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise
} }
/**
* Adds a new tab by cliking on the + button near the tab bar.
*/
async addNewTab() { async addNewTab() {
await this.page.locator('[data-trigger-command="openNewTab"]').click(); await this.page.locator('[data-trigger-command="openNewTab"]').click();
} }
/**
* Looks for a given title in the note tree and clicks on it. Useful for selecting option pages in settings in a similar fashion as the user.
* @param title the title of the note to click, as displayed in the note tree.
*/
async clickNoteOnNoteTreeByTitle(title: string) { async clickNoteOnNoteTreeByTitle(title: string) {
this.noteTree.getByText(title).click(); await this.noteTree.getByText(title).click();
}
/**
* Executes any Trilium command on the client.
* @param command the command to send.
*/
async triggerCommand(command: string) {
await this.page.evaluate(async (command: string) => {
await (window as any).glob.appContext.triggerCommand(command);
}, command);
}
async setOption(key: string, value: string) {
const csrfToken = await this.page.evaluate(() => {
return (window as any).glob.csrfToken;
});
expect(csrfToken).toBeTruthy();
await expect(await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, {
headers: {
"x-csrf-token": csrfToken
}
})).toBeOK();
} }
} }

Binary file not shown.

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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": {}
}

View File

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

View File

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

3417
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@
"start-server": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/main.ts", "start-server": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/main.ts",
"start-server-safe": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/main.ts", "start-server-safe": "cross-env TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/main.ts",
"start-server-no-dir": "cross-env TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/main.ts", "start-server-no-dir": "cross-env TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/main.ts",
"start-test-server": "npm run switch-server; rimraf ./data-test; cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 ts-node src/main.ts", "start-test-server": "npm run switch-server && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
"qstart-server": "npm run switch-server && npm run start-server", "qstart-server": "npm run switch-server && npm run start-server",
"start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .", "start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .",
"start-electron-nix": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"", "start-electron-nix": "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 .\"",
@ -31,14 +31,14 @@
"start-electron-no-dir-nix": "npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"", "start-electron-no-dir-nix": "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", "qstart-electron": "npm run switch-electron && npm run start-electron",
"switch-server": "rimraf ./node_modules/better-sqlite3 && npm install", "switch-server": "rimraf ./node_modules/better-sqlite3 && npm install",
"switch-electron": "npx electron-rebuild", "switch-electron": "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-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-jasmine": "cross-env TRILIUM_DATA_DIR=./data-test tsx ./node_modules/jasmine/bin/jasmine.js", "test-playwright": "playwright test",
"test-es6": "tsx -r esm spec-es6/attribute_parser.spec.ts", "test": "cross-env TRILIUM_DATA_DIR=./data-test vitest",
"test": "npm run test-jasmine && npm run test-es6", "test-coverage": "cross-env TRILIUM_DATA_DIR=./data-test vitest --coverage",
"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",
@ -65,7 +65,7 @@
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"autocomplete.js": "0.38.1", "autocomplete.js": "0.38.1",
"axios": "1.7.9", "axios": "1.7.9",
"better-sqlite3": "11.7.2", "better-sqlite3": "11.8.0",
"bootstrap": "5.3.3", "bootstrap": "5.3.3",
"boxicons": "2.1.4", "boxicons": "2.1.4",
"cheerio": "1.0.0", "cheerio": "1.0.0",
@ -74,7 +74,7 @@
"codemirror": "5.65.18", "codemirror": "5.65.18",
"compression": "1.7.5", "compression": "1.7.5",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"csurf": "1.11.0", "csrf-csrf": "3.1.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2", "dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0", "debounce": "2.2.0",
@ -89,7 +89,7 @@
"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.47.2",
"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",
@ -115,7 +115,7 @@
"marked": "15.0.6", "marked": "15.0.6",
"mermaid": "11.4.1", "mermaid": "11.4.1",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"mind-elixir": "4.3.5", "mind-elixir": "4.3.6",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"normalize-strings": "1.1.1", "normalize-strings": "1.1.1",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
@ -136,7 +136,7 @@
"stream-throttle": "0.1.3", "stream-throttle": "0.1.3",
"striptags": "3.2.0", "striptags": "3.2.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"ts-loader": "9.5.1", "ts-loader": "9.5.2",
"turndown": "7.2.0", "turndown": "7.2.0",
"unescape": "1.0.1", "unescape": "1.0.1",
"vanilla-js-wheel-zoom": "9.0.4", "vanilla-js-wheel-zoom": "9.0.4",
@ -151,6 +151,7 @@
"@electron-forge/maker-squirrel": "7.6.0", "@electron-forge/maker-squirrel": "7.6.0",
"@electron-forge/maker-zip": "7.6.0", "@electron-forge/maker-zip": "7.6.0",
"@electron-forge/plugin-auto-unpack-natives": "7.6.0", "@electron-forge/plugin-auto-unpack-natives": "7.6.0",
"@electron/rebuild": "3.7.1",
"@playwright/test": "1.49.1", "@playwright/test": "1.49.1",
"@types/archiver": "6.0.3", "@types/archiver": "6.0.3",
"@types/better-sqlite3": "7.6.12", "@types/better-sqlite3": "7.6.12",
@ -159,7 +160,6 @@
"@types/cls-hooked": "4.3.9", "@types/cls-hooked": "4.3.9",
"@types/compression": "1.7.5", "@types/compression": "1.7.5",
"@types/cookie-parser": "1.4.8", "@types/cookie-parser": "1.4.8",
"@types/csurf": "1.11.5",
"@types/debounce": "1.2.4", "@types/debounce": "1.2.4",
"@types/ejs": "3.1.5", "@types/ejs": "3.1.5",
"@types/electron-squirrel-startup": "1.0.2", "@types/electron-squirrel-startup": "1.0.2",
@ -174,7 +174,7 @@
"@types/jsdom": "21.1.7", "@types/jsdom": "21.1.7",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/multer": "1.4.12", "@types/multer": "1.4.12",
"@types/node": "22.10.5", "@types/node": "22.10.7",
"@types/safe-compare": "1.1.2", "@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.13.0", "@types/sanitize-html": "2.13.0",
"@types/sax": "1.2.7", "@types/sax": "1.2.7",
@ -188,12 +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": "33.3.1", "electron": "34.0.0",
"electron-packager": "17.1.2",
"electron-rebuild": "3.2.9",
"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",
@ -201,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"

View File

@ -28,7 +28,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
baseURL: (!process.env.TRILIUM_DOCKER ? SERVER_URL : "http://127.0.0.1:8080"), baseURL: SERVER_URL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',

View File

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

156
spec-es6/data_dir.spec.ts Normal file
View File

@ -0,0 +1,156 @@
import { describe, it, expect } from "vitest";
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.todo("#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 mockDataDirBase = "/home/test/MOCK_TRILIUM_DATA_DIR"
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
result.BACKUP_DIR = "attempt to change";
return result.BACKUP_DIR;
}
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)
}
})
})
});

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
describe("Notes", () => { import { describe, it } from "vitest";
describe.todo("Notes", () => {
it("zzz", () => {}); it("zzz", () => {});
}); });

View File

@ -3,7 +3,7 @@ import BBranch from "../../src/becca/entities/bbranch.js";
import BAttribute from "../../src/becca/entities/battribute.js"; import BAttribute from "../../src/becca/entities/battribute.js";
import becca from "../../src/becca/becca.js"; import becca from "../../src/becca/becca.js";
import randtoken from "rand-token"; import randtoken from "rand-token";
import SearchResult from "../../src/services/search/search_result.js"; import type SearchResult from "../../src/services/search/search_result.js";
import type { NoteType } from "../../src/becca/entities/rows.js"; import type { NoteType } from "../../src/becca/entities/rows.js";
randtoken.generator({ source: "crypto" }); randtoken.generator({ source: "crypto" });

View File

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

View File

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

View File

@ -1,6 +1,7 @@
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 Expression from "../../src/services/search/expressions/expression.js"; import type Expression from "../../src/services/search/expressions/expression.js";
import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js"; import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js";
import NotExp from "../../src/services/search/expressions/not.js"; import NotExp from "../../src/services/search/expressions/not.js";
import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js"; import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js";

View File

@ -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();
@ -549,7 +550,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 +563,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");

View File

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

View File

@ -1,4 +1,4 @@
import child_process from "child_process"; import type child_process from "child_process";
let etapiAuthToken: string | undefined; let etapiAuthToken: string | undefined;

View File

@ -1,10 +0,0 @@
{
"spec_dir": "",
"spec_files": [
"spec/**/*.spec.ts",
"src/**/*.spec.ts"
],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": true
}

View File

@ -1,3 +1,4 @@
import { describe, it, expect } from "vitest";
import { trimIndentation } from "./utils.js"; import { trimIndentation } from "./utils.js";
describe("Utils", () => { describe("Utils", () => {

View File

@ -1,17 +1,17 @@
import sql from "../services/sql.js"; import sql from "../services/sql.js";
import NoteSet from "../services/search/note_set.js"; import NoteSet from "../services/search/note_set.js";
import NotFoundError from "../errors/not_found_error.js"; import NotFoundError from "../errors/not_found_error.js";
import BOption from "./entities/boption.js"; import type BOption from "./entities/boption.js";
import BNote from "./entities/bnote.js"; import type BNote from "./entities/bnote.js";
import BEtapiToken from "./entities/betapi_token.js"; import type BEtapiToken from "./entities/betapi_token.js";
import BAttribute from "./entities/battribute.js"; import type BAttribute from "./entities/battribute.js";
import BBranch from "./entities/bbranch.js"; import type BBranch from "./entities/bbranch.js";
import BRevision from "./entities/brevision.js"; import BRevision from "./entities/brevision.js";
import BAttachment from "./entities/battachment.js"; import BAttachment from "./entities/battachment.js";
import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js"; import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js";
import BBlob from "./entities/bblob.js"; import BBlob from "./entities/bblob.js";
import BRecentNote from "./entities/brecent_note.js"; import BRecentNote from "./entities/brecent_note.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
interface AttachmentOpts { interface AttachmentOpts {
includeContentLength?: boolean; includeContentLength?: boolean;

View File

@ -12,7 +12,7 @@ import BEtapiToken from "./entities/betapi_token.js";
import cls from "../services/cls.js"; import cls from "../services/cls.js";
import entityConstructor from "../becca/entity_constructor.js"; import entityConstructor from "../becca/entity_constructor.js";
import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js"; import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
const beccaLoaded = new Promise<void>(async (res, rej) => { const beccaLoaded = new Promise<void>(async (res, rej) => {

View File

@ -9,7 +9,7 @@ import cls from "../../services/cls.js";
import log from "../../services/log.js"; import log from "../../services/log.js";
import protectedSessionService from "../../services/protected_session.js"; import protectedSessionService from "../../services/protected_session.js";
import blobService from "../../services/blob.js"; import blobService from "../../services/blob.js";
import Becca, { type ConstructorData } from "../becca-interface.js"; import type { default as Becca, ConstructorData } from "../becca-interface.js";
import becca from "../becca.js"; import becca from "../becca.js";
interface ContentOpts { interface ContentOpts {

View File

@ -7,8 +7,8 @@ import sql from "../../services/sql.js";
import protectedSessionService from "../../services/protected_session.js"; import protectedSessionService from "../../services/protected_session.js";
import log from "../../services/log.js"; import log from "../../services/log.js";
import type { AttachmentRow } from "./rows.js"; import type { AttachmentRow } from "./rows.js";
import BNote from "./bnote.js"; import type BNote from "./bnote.js";
import BBranch from "./bbranch.js"; import type BBranch from "./bbranch.js";
import noteService from "../../services/notes.js"; import noteService from "../../services/notes.js";
const attachmentRoleToNoteTypeMapping = { const attachmentRoleToNoteTypeMapping = {

View File

@ -15,7 +15,7 @@ import dayjs from "dayjs";
import utc from "dayjs/plugin/utc.js"; import utc from "dayjs/plugin/utc.js";
import eventService from "../../services/events.js"; import eventService from "../../services/events.js";
import type { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from "./rows.js"; import type { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from "./rows.js";
import BBranch from "./bbranch.js"; import type BBranch from "./bbranch.js";
import BAttribute from "./battribute.js"; import BAttribute from "./battribute.js";
import type { NotePojo } from "../becca-interface.js"; import type { NotePojo } from "../becca-interface.js";
import searchService from "../../services/search/services/search.js"; import searchService from "../../services/search/services/search.js";

View File

@ -1,5 +1,5 @@
import type { ConstructorData } from "./becca-interface.js"; import type { ConstructorData } from "./becca-interface.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import BAttachment from "./entities/battachment.js"; import BAttachment from "./entities/battachment.js";
import BAttribute from "./entities/battribute.js"; import BAttribute from "./entities/battribute.js";
import BBlob from "./entities/bblob.js"; import BBlob from "./entities/bblob.js";

View File

@ -3,7 +3,7 @@ import log from "../services/log.js";
import beccaService from "./becca_service.js"; import beccaService from "./becca_service.js";
import dateUtils from "../services/date_utils.js"; import dateUtils from "../services/date_utils.js";
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import BNote from "./entities/bnote.js"; import type BNote from "./entities/bnote.js";
const DEBUG = false; const DEBUG = false;

View File

@ -1,4 +1,4 @@
import { Router } from "express"; import type { Router } from "express";
import appInfo from "../services/app_info.js"; import appInfo from "../services/app_info.js";
import eu from "./etapi_utils.js"; import eu from "./etapi_utils.js";

View File

@ -3,7 +3,7 @@ import eu from "./etapi_utils.js";
import mappers from "./mappers.js"; import mappers from "./mappers.js";
import v from "./validators.js"; import v from "./validators.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import { Router } from "express"; import type { Router } from "express";
import type { AttachmentRow } from "../becca/entities/rows.js"; import type { AttachmentRow } from "../becca/entities/rows.js";
import type { ValidatorMap } from "./etapi-interface.js"; import type { ValidatorMap } from "./etapi-interface.js";

View File

@ -3,7 +3,7 @@ import eu from "./etapi_utils.js";
import mappers from "./mappers.js"; import mappers from "./mappers.js";
import attributeService from "../services/attributes.js"; import attributeService from "../services/attributes.js";
import v from "./validators.js"; import v from "./validators.js";
import { Router } from "express"; import type { Router } from "express";
import type { AttributeRow } from "../becca/entities/rows.js"; import type { AttributeRow } from "../becca/entities/rows.js";
import type { ValidatorMap } from "./etapi-interface.js"; import type { ValidatorMap } from "./etapi-interface.js";

View File

@ -1,4 +1,4 @@
import { Router } from "express"; import type { Router } from "express";
import eu from "./etapi_utils.js"; import eu from "./etapi_utils.js";
import backupService from "../services/backup.js"; import backupService from "../services/backup.js";

View File

@ -1,4 +1,4 @@
import { Router } from "express"; import type { Router } from "express";
import becca from "../becca/becca.js"; import becca from "../becca/becca.js";
import eu from "./etapi_utils.js"; import eu from "./etapi_utils.js";

View File

@ -1,7 +1,7 @@
import BAttachment from "../becca/entities/battachment.js"; import type BAttachment from "../becca/entities/battachment.js";
import BAttribute from "../becca/entities/battribute.js"; import type BAttribute from "../becca/entities/battribute.js";
import BBranch from "../becca/entities/bbranch.js"; import type BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js"; import type BNote from "../becca/entities/bnote.js";
function mapNoteToPojo(note: BNote) { function mapNoteToPojo(note: BNote) {
return { return {

View File

@ -9,7 +9,7 @@ import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js"; import SearchContext from "../services/search/search_context.js";
import zipExportService from "../services/export/zip.js"; import zipExportService from "../services/export/zip.js";
import zipImportService from "../services/import/zip.js"; import zipImportService from "../services/import/zip.js";
import { type Request, Router } from "express"; import type { Request, Router } from "express";
import type { ParsedQs } from "qs"; import type { ParsedQs } from "qs";
import type { NoteParams } from "../services/note-interface.js"; import type { NoteParams } from "../services/note-interface.js";
import type { SearchParams } from "../services/search/services/types.js"; import type { SearchParams } from "../services/search/services/types.js";

View File

@ -1,4 +1,4 @@
import { Router } from "express"; import type { Router } from "express";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";

View File

@ -2,7 +2,7 @@ import specialNotesService from "../services/special_notes.js";
import dateNotesService from "../services/date_notes.js"; import dateNotesService from "../services/date_notes.js";
import eu from "./etapi_utils.js"; import eu from "./etapi_utils.js";
import mappers from "./mappers.js"; import mappers from "./mappers.js";
import { Router } from "express"; import type { Router } from "express";
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`); const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`); const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);

View File

@ -14,15 +14,15 @@ import MainTreeExecutors from "./main_tree_executors.js";
import toast from "../services/toast.js"; import toast from "../services/toast.js";
import ShortcutComponent from "./shortcut_component.js"; import ShortcutComponent from "./shortcut_component.js";
import { t, initLocale } from "../services/i18n.js"; import { t, initLocale } from "../services/i18n.js";
import NoteDetailWidget from "../widgets/note_detail.js"; import type NoteDetailWidget from "../widgets/note_detail.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js"; import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
import type { Node } from "../services/tree.js"; import type { Node } from "../services/tree.js";
import LoadResults from "../services/load_results.js"; import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js"; import type { Attribute } from "../services/attribute_parser.js";
import NoteTreeWidget from "../widgets/note_tree.js"; import type NoteTreeWidget from "../widgets/note_tree.js";
import NoteContext, { type GetTextEditorCallback } from "./note_context.js"; import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget; getRootWidget: (appContext: AppContext) => RootWidget;
@ -92,7 +92,7 @@ export type CommandMappings = {
filePath: string; filePath: string;
}; };
focusAndSelectTitle: CommandData & { focusAndSelectTitle: CommandData & {
isNewNote: boolean; isNewNote?: boolean;
}; };
showPromptDialog: PromptDialogOptions; showPromptDialog: PromptDialogOptions;
showInfoDialog: ConfirmWithMessageOptions; showInfoDialog: ConfirmWithMessageOptions;
@ -262,6 +262,9 @@ type EventMappings = {
}; };
noteContextRemovedEvent: { noteContextRemovedEvent: {
ntxIds: string[]; ntxIds: string[];
};
exportSvg: {
ntxId: string;
} }
}; };
@ -274,15 +277,16 @@ export type CommandListener<T extends CommandNames> = {
}; };
export type CommandListenerData<T extends CommandNames> = CommandMappings[T]; export type CommandListenerData<T extends CommandNames> = CommandMappings[T];
export type EventData<T extends EventNames> = EventMappings[T];
type CommandAndEventMappings = CommandMappings & EventMappings; type CommandAndEventMappings = CommandMappings & EventMappings;
type EventOnlyNames = keyof EventMappings;
export type EventNames = CommandNames | EventOnlyNames;
export type EventData<T extends EventNames> = CommandAndEventMappings[T];
/** /**
* This type is a discriminated union which contains all the possible commands that can be triggered via {@link AppContext.triggerCommand}. * This type is a discriminated union which contains all the possible commands that can be triggered via {@link AppContext.triggerCommand}.
*/ */
export type CommandNames = keyof CommandMappings; export type CommandNames = keyof CommandMappings;
type EventNames = keyof EventMappings;
type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType ? K : never }[keyof T]; type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType ? K : never }[keyof T];
@ -375,12 +379,10 @@ class AppContext extends Component {
this.child(rootWidget); this.child(rootWidget);
this.triggerEvent("initialRenderComplete"); this.triggerEvent("initialRenderComplete", {});
} }
// TODO: Remove ignore once all commands are mapped out. triggerEvent<K extends EventNames>(name: K, data: EventData<K>) {
//@ts-ignore
triggerEvent<K extends EventNames | CommandNames>(name: K, data: CommandAndEventMappings[K] = {}) {
return this.handleEvent(name, data); return this.handleEvent(name, data);
} }

View File

@ -1,5 +1,5 @@
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import type { CommandMappings, CommandNames } from "./app_context.js"; import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
/** /**
* Abstract class for all components in the Trilium's frontend. * Abstract class for all components in the Trilium's frontend.
@ -46,7 +46,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return this; return this;
} }
handleEvent(name: string, data: unknown): Promise<unknown> | null { handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
try { try {
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data); const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
@ -65,11 +65,11 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return this.parent?.triggerEvent(name, data); return this.parent?.triggerEvent(name, data);
} }
handleEventInChildren(name: string, data: unknown = {}) { handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
const promises = []; const promises: Promise<unknown>[] = [];
for (const child of this.children) { for (const child of this.children) {
const ret = child.handleEvent(name, data); const ret = child.handleEvent(name, data) as Promise<void>;
if (ret) { if (ret) {
promises.push(ret); promises.push(ret);

View File

@ -10,7 +10,7 @@ import bundleService from "../services/bundle.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import linkService from "../services/link.js"; import linkService from "../services/link.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
// TODO: Move somewhere else nicer. // TODO: Move somewhere else nicer.
export type SqlExecuteResults = unknown[]; export type SqlExecuteResults = unknown[];
@ -114,11 +114,9 @@ export default class Entrypoints extends Component {
utils.reloadFrontendApp(); utils.reloadFrontendApp();
} }
logoutCommand() { async logoutCommand() {
const $logoutForm = $('<form action="logout" method="POST">').append($(`<input type='_hidden' name="_csrf" value="${glob.csrfToken}"/>`)); await server.post("../logout");
window.location.replace(`/login`);
$("body").append($logoutForm);
$logoutForm.trigger("submit");
} }
backInNoteHistoryCommand() { backInNoteHistoryCommand() {

View File

@ -8,7 +8,7 @@ import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js"; import hoistedNoteService from "../services/hoisted_note.js";
import options from "../services/options.js"; import options from "../services/options.js";
import type { ViewScope } from "../services/link.js"; import type { ViewScope } from "../services/link.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
interface SetNoteOpts { interface SetNoteOpts {
triggerSwitchEvent?: unknown; triggerSwitchEvent?: unknown;
@ -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) {

View File

@ -5,8 +5,8 @@ import froca from "../services/froca.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js"; import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js"; import type { Froca } from "../services/froca-interface.js";
import FAttachment from "./fattachment.js"; import type FAttachment from "./fattachment.js";
import FAttribute, { type AttributeType } from "./fattribute.js"; import type { default as FAttribute, AttributeType } from "./fattribute.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
const LABEL = "label"; const LABEL = "label";
@ -35,7 +35,7 @@ const NOTE_TYPE_ICONS = {
* end user. Those types should be used only for checking against, they are * end user. Those types should be used only for checking against, they are
* not for direct use. * not for direct use.
*/ */
type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code"; type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap";
interface NotePathRecord { interface NotePathRecord {
isArchived: boolean; isArchived: boolean;

View File

@ -27,7 +27,7 @@ import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolb
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import AboutDialog from "../widgets/dialogs/about.js"; import AboutDialog from "../widgets/dialogs/about.js";
import HelpDialog from "../widgets/dialogs/help.js"; import HelpDialog from "../widgets/dialogs/help.js";
import AppContext from "../components/app_context.js"; import type AppContext from "../components/app_context.js";
import TabRowWidget from "../widgets/tab_row.js"; import TabRowWidget from "../widgets/tab_row.js";
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js"; import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";

View File

@ -5,7 +5,7 @@ import dialogService from "../services/dialog.js";
import server from "../services/server.js"; import server from "../services/server.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import type { SelectMenuItemEventListener } from "../components/events.js"; import type { SelectMenuItemEventListener } from "../components/events.js";
import NoteTreeWidget from "../widgets/note_tree.js"; import type NoteTreeWidget from "../widgets/note_tree.js";
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js"; import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>; type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
@ -58,7 +58,7 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset } { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset }
]; ];
return items.filter((row) => row !== null); return items.filter((row) => row !== null) as MenuItem<LauncherCommandNames>[];
} }
async selectMenuItemHandler({ command }: MenuCommandItem<LauncherCommandNames>) { async selectMenuItemHandler({ command }: MenuCommandItem<LauncherCommandNames>) {

View File

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

View File

@ -9,8 +9,8 @@ import server from "../services/server.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js"; import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import NoteTreeWidget from "../widgets/note_tree.js"; import type NoteTreeWidget from "../widgets/note_tree.js";
import FAttachment from "../entities/fattachment.js"; import type FAttachment from "../entities/fattachment.js";
import type { SelectMenuItemEventListener } from "../components/events.js"; import type { SelectMenuItemEventListener } from "../components/events.js";
// TODO: Deduplicate once client/server is well split. // TODO: Deduplicate once client/server is well split.
@ -196,7 +196,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
enabled: notSearch && noSelectedNotes enabled: notSearch && noSelectedNotes
} }
]; ];
return items.filter((row) => row !== null); return items.filter((row) => row !== null) as MenuItem<TreeCommandNames>[];
} }
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) { async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {

View File

@ -1,12 +1,16 @@
import type { EntityRowNames } from "./services/load_results.js"; import type { EntityRowNames } from "./services/load_results.js";
interface Entity {
isDeleted?: boolean;
}
// TODO: Deduplicate with src/services/entity_changes_interface.ts // TODO: Deduplicate with src/services/entity_changes_interface.ts
export interface EntityChange { export interface EntityChange {
id?: number | null; id?: number | null;
noteId?: string; noteId?: string;
entityName: EntityRowNames; entityName: EntityRowNames;
entityId: string; entityId: string;
entity?: any; entity?: Entity;
positions?: Record<string, number>; positions?: Record<string, number>;
hash: string; hash: string;
utcDateChanged?: string; utcDateChanged?: string;

View File

@ -1,7 +1,7 @@
import ws from "./ws.js"; import ws from "./ws.js";
import froca from "./froca.js"; import froca from "./froca.js";
import FAttribute from "../entities/fattribute.js"; import type FAttribute from "../entities/fattribute.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
async function renderAttribute(attribute: FAttribute, renderIsInheritable: boolean) { async function renderAttribute(attribute: FAttribute, renderIsInheritable: boolean) {
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : ""; const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : "";

View File

@ -1,6 +1,6 @@
import server from "./server.js"; import server from "./server.js";
import froca from "./froca.js"; import froca from "./froca.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import type { AttributeRow } from "./load_results.js"; import type { AttributeRow } from "./load_results.js";
async function addLabel(noteId: string, name: string, value: string = "") { async function addLabel(noteId: string, name: string, value: string = "") {

View File

@ -14,7 +14,7 @@ import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js"; import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
const ACTION_GROUPS = [ const ACTION_GROUPS = [
{ {

View File

@ -1,8 +1,8 @@
import FAttachment from "../entities/fattachment.js"; import type FAttachment from "../entities/fattachment.js";
import FAttribute from "../entities/fattribute.js"; import type FAttribute from "../entities/fattribute.js";
import FBlob from "../entities/fblob.js"; import type FBlob from "../entities/fblob.js";
import FBranch from "../entities/fbranch.js"; import type FBranch from "../entities/fbranch.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
export interface Froca { export interface Froca {
notes: Record<string, FNote>; notes: Record<string, FNote>;

View File

@ -6,7 +6,7 @@ import noteAttributeCache from "./note_attribute_cache.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js"; import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FNote, { type FNoteRow } from "../entities/fnote.js"; import type { default as FNote, FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js"; import type { EntityChange } from "../server_types.js";
async function processEntityChanges(entityChanges: EntityChange[]) { async function processEntityChanges(entityChanges: EntityChange[]) {
@ -290,6 +290,7 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) {
return; return;
} }
if (ec.entity) {
if (attachment) { if (attachment) {
attachment.update(ec.entity as FAttachmentRow); attachment.update(ec.entity as FAttachmentRow);
} else { } else {
@ -300,6 +301,7 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) {
note.attachments.push(new FAttachment(froca, attachmentRow)); note.attachments.push(new FAttachment(froca, attachmentRow));
} }
} }
}
loadResults.addAttachmentRow(attachmentEntity); loadResults.addAttachmentRow(attachmentEntity);
} }

View File

@ -15,11 +15,11 @@ import BasicWidget from "../widgets/basic_widget.js";
import SpacedUpdate from "./spaced_update.js"; import SpacedUpdate from "./spaced_update.js";
import shortcutService from "./shortcuts.js"; import shortcutService from "./shortcuts.js";
import dialogService from "./dialog.js"; import dialogService from "./dialog.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import NoteContext from "../components/note_context.js"; import type NoteContext from "../components/note_context.js";
import NoteDetailWidget from "../widgets/note_detail.js"; import type NoteDetailWidget from "../widgets/note_detail.js";
import Component from "../components/component.js"; import type Component from "../components/component.js";
/** /**
* A whole number * A whole number
@ -463,7 +463,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext().setNote(notePath);
await appContext.triggerEvent("focusAndSelectTitle"); await appContext.triggerEvent("focusAndSelectTitle", {});
}; };
this.openTabWithNote = async (notePath, activate) => { this.openTabWithNote = async (notePath, activate) => {
@ -472,7 +472,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, { activate }); await appContext.tabManager.openTabWithNoteWithHoisting(notePath, { activate });
if (activate) { if (activate) {
await appContext.triggerEvent("focusAndSelectTitle"); await appContext.triggerEvent("focusAndSelectTitle", {});
} }
}; };
@ -485,7 +485,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath }); await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath });
if (activate) { if (activate) {
await appContext.triggerEvent("focusAndSelectTitle"); await appContext.triggerEvent("focusAndSelectTitle", {});
} }
}; };

View File

@ -2,7 +2,7 @@ import appContext from "../components/app_context.js";
import treeService, { type Node } from "./tree.js"; import treeService, { type Node } from "./tree.js";
import dialogService from "./dialog.js"; import dialogService from "./dialog.js";
import froca from "./froca.js"; import froca from "./froca.js";
import NoteContext from "../components/note_context.js"; import type NoteContext from "../components/note_context.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
function getHoistedNoteId() { function getHoistedNoteId() {

View File

@ -1,7 +1,7 @@
import server from "./server.js"; import server from "./server.js";
import appContext, { type CommandNames } from "../components/app_context.js"; import appContext, { type CommandNames } from "../components/app_context.js";
import shortcutService from "./shortcuts.js"; import shortcutService from "./shortcuts.js";
import Component from "../components/component.js"; import type Component from "../components/component.js";
const keyboardActionRepo: Record<string, Action> = {}; const keyboardActionRepo: Record<string, Action> = {};

View File

@ -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"]
}; };
@ -96,10 +84,6 @@ const I18NEXT: Library = {
js: ["node_modules/i18next/i18next.min.js", "node_modules/i18next-http-backend/i18nextHttpBackend.min.js"] js: ["node_modules/i18next/i18next.min.js", "node_modules/i18next-http-backend/i18nextHttpBackend.min.js"]
}; };
const MIND_ELIXIR: Library = {
js: ["node_modules/mind-elixir/dist/MindElixir.iife.js", "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs"]
};
const HIGHLIGHT_JS: Library = { const HIGHLIGHT_JS: Library = {
js: () => { js: () => {
const mimeTypes = mimeTypesService.getMimeTypes(); const mimeTypes = mimeTypesService.getMimeTypes();
@ -213,12 +197,9 @@ export default {
CALENDAR_WIDGET, CALENDAR_WIDGET,
KATEX, KATEX,
WHEEL_ZOOM, WHEEL_ZOOM,
FORCE_GRAPH,
MERMAID, MERMAID,
MERMAID_ELK,
EXCALIDRAW, EXCALIDRAW,
MARKJS, MARKJS,
I18NEXT, I18NEXT,
MIND_ELIXIR,
HIGHLIGHT_JS HIGHLIGHT_JS
}; };

View File

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

View File

@ -1,4 +1,4 @@
import FAttribute from "../entities/fattribute.js"; import type FAttribute from "../entities/fattribute.js";
/** /**
* The purpose of this class is to cache the list of attributes for notes. * The purpose of this class is to cache the list of attributes for notes.

View File

@ -6,8 +6,8 @@ import froca from "./froca.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import toastService from "./toast.js"; import toastService from "./toast.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import FBranch from "../entities/fbranch.js"; import type FBranch from "../entities/fbranch.js";
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
interface CreateNoteOpts { interface CreateNoteOpts {

View File

@ -5,7 +5,7 @@ import attributeRenderer from "./attribute_renderer.js";
import libraryLoader from "./library_loader.js"; import libraryLoader from "./library_loader.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import utils from "./utils.js"; import utils from "./utils.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
const TPL = ` const TPL = `
<div class="note-list"> <div class="note-list">

View File

@ -5,7 +5,7 @@ import utils from "./utils.js";
import attributeRenderer from "./attribute_renderer.js"; import attributeRenderer from "./attribute_renderer.js";
import contentRenderer from "./content_renderer.js"; import contentRenderer from "./content_renderer.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
function setupGlobalTooltip() { function setupGlobalTooltip() {

View File

@ -74,9 +74,9 @@ ws.subscribeToMessages(async (message) => {
if (message.type === "protectedSessionLogin") { if (message.type === "protectedSessionLogin") {
await reloadData(); await reloadData();
await appContext.triggerEvent("frocaReloaded"); await appContext.triggerEvent("frocaReloaded", {});
appContext.triggerEvent("protectedSessionStarted"); appContext.triggerEvent("protectedSessionStarted", {});
appContext.triggerCommand("closeProtectedSessionPasswordDialog"); appContext.triggerCommand("closeProtectedSessionPasswordDialog");

View File

@ -1,4 +1,4 @@
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import server from "./server.js"; import server from "./server.js";
function enableProtectedSession() { function enableProtectedSession() {

View File

@ -1,6 +1,6 @@
import server from "./server.js"; import server from "./server.js";
import bundleService, { type Bundle } from "./bundle.js"; import bundleService, { type Bundle } from "./bundle.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
async function render(note: FNote, $el: JQuery<HTMLElement>) { async function render(note: FNote, $el: JQuery<HTMLElement>) {
const relations = note.getRelations("renderNote"); const relations = note.getRelations("renderNote");

View File

@ -20,3 +20,7 @@ declare module "draggabilly" {
destroy(); destroy();
} }
} }
declare module '@mind-elixir/node-menu' {
export default mindmap;
}

View File

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

View File

@ -3,17 +3,17 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import contextMenuService from "../../menus/context_menu.js"; import contextMenuService from "../../menus/context_menu.js";
import attributeParser from "../../services/attribute_parser.js"; import attributeParser, { type Attribute } from "../../services/attribute_parser.js";
import libraryLoader from "../../services/library_loader.js"; import libraryLoader from "../../services/library_loader.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import attributeRenderer from "../../services/attribute_renderer.js"; import attributeRenderer from "../../services/attribute_renderer.js";
import noteCreateService from "../../services/note_create.js"; import noteCreateService from "../../services/note_create.js";
import attributeService from "../../services/attributes.js"; import attributeService from "../../services/attributes.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import AttributeDetailWidget from "./attribute_detail.js"; import type AttributeDetailWidget from "./attribute_detail.js";
import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js"; import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js";
import FAttribute, { type AttributeType } from "../../entities/fattribute.js"; import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
import FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
const HELP_TEXT = ` const HELP_TEXT = `
<p>${t("attribute_editor.help_text_body1")}</p> <p>${t("attribute_editor.help_text_body1")}</p>
@ -417,7 +417,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
return null; return null;
} }
let matchedAttr = null; let matchedAttr: Attribute | null = null;
for (const attr of parsedAttrs) { for (const attr of parsedAttrs) {
if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) {

View File

@ -2,7 +2,7 @@ import { t } from "../../services/i18n.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import ws from "../../services/ws.js"; import ws from "../../services/ws.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import FAttribute from "../../entities/fattribute.js"; import type FAttribute from "../../entities/fattribute.js";
interface ActionDefinition { interface ActionDefinition {
script: string; script: string;

View File

@ -1,4 +1,4 @@
import Component, { TypedComponent } from "../../components/component.js"; import type { default as Component, TypedComponent } from "../../components/component.js";
import BasicWidget, { TypedBasicWidget } from "../basic_widget.js"; import BasicWidget, { TypedBasicWidget } from "../basic_widget.js";
export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> { export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> {

View File

@ -1,4 +1,4 @@
import { TypedComponent } from "../../components/component.js"; import type { TypedComponent } from "../../components/component.js";
import Container from "./container.js"; import Container from "./container.js";
export type FlexDirection = "row" | "column"; export type FlexDirection = "row" | "column";

View File

@ -11,7 +11,7 @@ import utils from "../../services/utils.js";
import TodayLauncher from "../buttons/launcher/today_launcher.js"; import TodayLauncher from "../buttons/launcher/today_launcher.js";
import HistoryNavigationButton from "../buttons/history_navigation.js"; import HistoryNavigationButton from "../buttons/history_navigation.js";
import QuickSearchLauncherWidget from "../quick_search_launcher.js"; import QuickSearchLauncherWidget from "../quick_search_launcher.js";
import FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { CommandNames } from "../../components/app_context.js"; import type { CommandNames } from "../../components/app_context.js";
interface InnerWidget extends BasicWidget { interface InnerWidget extends BasicWidget {

View File

@ -51,7 +51,7 @@ export default class LauncherContainer extends FlexContainer<LauncherWidget> {
this.$widget.empty(); this.$widget.empty();
this.renderChildren(); this.renderChildren();
await this.handleEventInChildren("initialRenderComplete"); await this.handleEventInChildren("initialRenderComplete", {});
const activeContext = appContext.tabManager.getActiveContext(); const activeContext = appContext.tabManager.getActiveContext();

View File

@ -1,6 +1,7 @@
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
import splitService from "../../services/resizer.js"; import splitService from "../../services/resizer.js";
import RightPanelWidget from "../right_panel_widget.js"; import type RightPanelWidget from "../right_panel_widget.js";
import type { EventData, EventNames } from "../../components/app_context.js";
export default class RightPaneContainer extends FlexContainer<RightPanelWidget> { export default class RightPaneContainer extends FlexContainer<RightPanelWidget> {
private rightPaneHidden: boolean; private rightPaneHidden: boolean;
@ -19,7 +20,7 @@ export default class RightPaneContainer extends FlexContainer<RightPanelWidget>
return super.isEnabled() && !this.rightPaneHidden && this.children.length > 0 && !!this.children.find((ch) => ch.isEnabled() && ch.canBeShown()); return super.isEnabled() && !this.rightPaneHidden && this.children.length > 0 && !!this.children.find((ch) => ch.isEnabled() && ch.canBeShown());
} }
handleEventInChildren(name: string, data: unknown) { handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
const promise = super.handleEventInChildren(name, data); const promise = super.handleEventInChildren(name, data);
if (["activeContextChanged", "noteSwitchedAndActivated", "noteSwitched"].includes(name)) { if (["activeContextChanged", "noteSwitchedAndActivated", "noteSwitched"].includes(name)) {

View File

@ -12,7 +12,7 @@ import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js"; import OnClickButtonWidget from "./buttons/onclick_button.js";
import appContext, { type EventData } from "../components/app_context.js"; import appContext, { type EventData } from "../components/app_context.js";
import libraryLoader from "../services/library_loader.js"; import libraryLoader from "../services/library_loader.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
const TPL = `<div class="highlights-list-widget"> const TPL = `<div class="highlights-list-widget">
<style> <style>

View File

@ -4,6 +4,8 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js"; import server from "../services/server.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import { loadElkIfNeeded } from "../services/mermaid.js"; import { loadElkIfNeeded } from "../services/mermaid.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
const TPL = `<div class="mermaid-widget"> const TPL = `<div class="mermaid-widget">
<style> <style>
@ -39,8 +41,14 @@ const TPL = `<div class="mermaid-widget">
let idCounter = 1; let idCounter = 1;
export default class MermaidWidget extends NoteContextAwareWidget { export default class MermaidWidget extends NoteContextAwareWidget {
private $display!: JQuery<HTMLElement>;
private $errorContainer!: JQuery<HTMLElement>;
private $errorMessage!: JQuery<HTMLElement>;
private dirtyAttachment?: boolean;
isEnabled() { isEnabled() {
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope.viewMode === "default"; return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
} }
doRender() { doRender() {
@ -51,14 +59,14 @@ export default class MermaidWidget extends NoteContextAwareWidget {
this.$errorMessage = this.$errorContainer.find(".error-content"); this.$errorMessage = this.$errorContainer.find(".error-content");
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote) {
this.$errorContainer.hide(); this.$errorContainer.hide();
await libraryLoader.requireLibrary(libraryLoader.MERMAID); await libraryLoader.requireLibrary(libraryLoader.MERMAID);
mermaid.mermaidAPI.initialize({ mermaid.mermaidAPI.initialize({
startOnLoad: false, startOnLoad: false,
...getMermaidConfig() ...getMermaidConfig() as any
}); });
this.$display.empty(); this.$display.empty();
@ -91,12 +99,11 @@ export default class MermaidWidget extends NoteContextAwareWidget {
this.$display.attr("id", `mermaid-render-${idCounter}`); this.$display.attr("id", `mermaid-render-${idCounter}`);
WZoom.create(`#mermaid-render-${idCounter}`, { WZoom.create(`#mermaid-render-${idCounter}`, {
type: "html",
maxScale: 50, maxScale: 50,
speed: 1.3, speed: 1.3,
zoomOnClick: false zoomOnClick: false
}); });
} catch (e) { } catch (e: any) {
console.warn(e); console.warn(e);
this.$errorMessage.text(e.message); this.$errorMessage.text(e.message);
this.$errorContainer.show(); this.$errorContainer.show();
@ -106,24 +113,28 @@ export default class MermaidWidget extends NoteContextAwareWidget {
async renderSvg() { async renderSvg() {
idCounter++; idCounter++;
if (!this.note) {
return "";
}
const blob = await this.note.getBlob(); const blob = await this.note.getBlob();
const content = blob.content || ""; const content = blob?.content || "";
await loadElkIfNeeded(content); await loadElkIfNeeded(content);
const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content); const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
return svg; return svg;
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteContentReloaded(this.noteId)) { if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
this.dirtyAttachment = true; this.dirtyAttachment = true;
await this.refresh(); await this.refresh();
} }
} }
async exportSvgEvent({ ntxId }) { async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
if (!this.isNoteContext(ntxId) || this.note.type !== "mermaid") { if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid") {
return; return;
} }
@ -139,6 +150,7 @@ export function getMermaidConfig() {
return { return {
theme: mermaidTheme.trim(), theme: mermaidTheme.trim(),
securityLevel: "antiscript", securityLevel: "antiscript",
// TODO: Are all these options correct?
flow: { useMaxWidth: false }, flow: { useMaxWidth: false },
sequence: { useMaxWidth: false }, sequence: { useMaxWidth: false },
gantt: { useMaxWidth: false }, gantt: { useMaxWidth: false },

View File

@ -1,6 +1,6 @@
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import type { Screen } from "../../components/mobile_screen_switcher.js"; import type { Screen } from "../../components/mobile_screen_switcher.js";
import BasicWidget from "../basic_widget.js"; import type BasicWidget from "../basic_widget.js";
import FlexContainer, { type FlexDirection } from "../containers/flex_container.js"; import FlexContainer, { type FlexDirection } from "../containers/flex_container.js";
const DRAG_STATE_NONE = 0; const DRAG_STATE_NONE = 0;

View File

@ -1,7 +1,7 @@
import BasicWidget from "./basic_widget.js"; import BasicWidget from "./basic_widget.js";
import appContext, { type EventData } from "../components/app_context.js"; import appContext, { type EventData } from "../components/app_context.js";
import FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import NoteContext from "../components/note_context.js"; import type NoteContext from "../components/note_context.js";
/** /**
* This widget allows for changing and updating depending on the active note. * This widget allows for changing and updating depending on the active note.

View File

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

View File

@ -31,13 +31,26 @@ const TPL = `\
align-items: flex-end; align-items: flex-end;
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0;
right: 0; right: 0;
overflow-x: auto; overflow-x: auto;
overscroll-behavior: none;
z-index: 500; z-index: 500;
user-select: none; user-select: none;
} }
@media (max-width: 991px) {
body.mobile .classic-toolbar-widget.visible {
bottom: calc(var(--tab-bar-height) + var(--launcher-pane-height) + var(--mobile-bottom-offset));
}
}
@media (min-width: 991px) {
body.mobile .classic-toolbar-widget.visible {
bottom: 0;
left: 25%;
}
}
body.mobile .classic-toolbar-widget.dropdown-active { body.mobile .classic-toolbar-widget.dropdown-active {
height: 50vh; height: 50vh;
} }
@ -64,6 +77,9 @@ const TPL = `\
* The ribbon item is active by default for text notes, as long as they are not in read-only mode. * The ribbon item is active by default for text notes, as long as they are not in read-only mode.
*/ */
export default class ClassicEditorToolbar extends NoteContextAwareWidget { export default class ClassicEditorToolbar extends NoteContextAwareWidget {
private observer: MutationObserver;
constructor() { constructor() {
super(); super();
this.observer = new MutationObserver((e) => this.#onDropdownStateChanged(e)); this.observer = new MutationObserver((e) => this.#onDropdownStateChanged(e));
@ -83,7 +99,7 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
if (utils.isMobile()) { if (utils.isMobile()) {
// The virtual keyboard obscures the editing toolbar so we have to reposition by calculating the height of the keyboard. // The virtual keyboard obscures the editing toolbar so we have to reposition by calculating the height of the keyboard.
window.visualViewport.addEventListener("resize", () => this.#adjustPosition()); window.visualViewport?.addEventListener("resize", () => this.#adjustPosition());
window.addEventListener("scroll", () => this.#adjustPosition()); window.addEventListener("scroll", () => this.#adjustPosition());
// Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable. // Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable.
@ -95,13 +111,13 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
} }
} }
#onDropdownStateChanged(e) { #onDropdownStateChanged(e: MutationRecord[]) {
const dropdownActive = e.map((e) => e.target.ariaExpanded === "true").reduce((acc, e) => acc && e); const dropdownActive = e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e);
this.$widget[0].classList.toggle("dropdown-active", dropdownActive); this.$widget[0].classList.toggle("dropdown-active", dropdownActive);
} }
#adjustPosition() { #adjustPosition() {
let bottom = window.innerHeight - window.visualViewport.height; let bottom = window.innerHeight - (window.visualViewport?.height || 0);
if (bottom === 0) { if (bottom === 0) {
// The keyboard is not visible, align it to the launcher bar instead. // The keyboard is not visible, align it to the launcher bar instead.
@ -121,18 +137,25 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
} }
async #shouldDisplay() { async #shouldDisplay() {
if (options.get("textNoteEditorType") !== "ckeditor-classic") { if (utils.isDesktop() && options.get("textNoteEditorType") !== "ckeditor-classic") {
return false; return false;
} }
if (this.note.type !== "text") { if (!this.note || this.note.type !== "text") {
return false; return false;
} }
if (await this.noteContext.isReadOnly()) { if (await this.noteContext?.isReadOnly()) {
return false; return false;
} }
return true; return true;
} }
async refreshWithNote() {
if (utils.isMobile()) {
this.toggleExt(await this.#shouldDisplay());
}
}
} }

View File

@ -1,5 +1,5 @@
import BasicWidget from "./basic_widget.js"; import type BasicWidget from "./basic_widget.js";
import AbstractButtonWidget from "./buttons/abstract_button.js"; import type AbstractButtonWidget from "./buttons/abstract_button.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
const WIDGET_TPL = ` const WIDGET_TPL = `

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