mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-18 08:13:40 +08:00
Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms
This commit is contained in:
commit
5ad4092cb2
@ -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
|
||||||
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1,3 @@
|
|||||||
|
package-lock.json linguist-generated=true
|
||||||
**/package-lock.json linguist-generated=true
|
**/package-lock.json linguist-generated=true
|
||||||
|
libraries/** linguist-vendored
|
38
.github/workflows/dev.yml
vendored
38
.github/workflows/dev.yml
vendored
@ -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:
|
||||||
|
17
.github/workflows/main-docker.yml
vendored
17
.github/workflows/main-docker.yml
vendored
@ -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 --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,6 +83,15 @@ jobs:
|
|||||||
require-status: running
|
require-status: running
|
||||||
require-healthy: true
|
require-healthy: true
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: TRILIUM_DOCKER=1 npx playwright test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: Playwright report (${{ matrix.dockerfile }})
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
# Print the entire log of the container thus far, regardless if the healthcheck failed or succeeded
|
# Print the entire log of the container thus far, regardless if the healthcheck failed or succeeded
|
||||||
- name: Print entire log
|
- name: Print entire log
|
||||||
if: always()
|
if: always()
|
||||||
|
24
.github/workflows/main.yml
vendored
24
.github/workflows/main.yml
vendored
@ -72,9 +72,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}}
|
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}}
|
||||||
path: upload/*.${{ matrix.os.extension }}
|
path: upload/*.${{ matrix.os.extension }}
|
||||||
build_linux_server-x64:
|
build_linux_server:
|
||||||
name: Build Linux Server x86_64
|
name: Build Linux Server
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [x64, arm64]
|
||||||
|
include:
|
||||||
|
- arch: x64
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
- arch: arm64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
@ -84,17 +93,18 @@ jobs:
|
|||||||
cache: "npm"
|
cache: "npm"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run Linux server build (x86_64)
|
- name: Run Linux server build
|
||||||
|
env:
|
||||||
|
MATRIX_ARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
npm run update-build-info
|
npm run update-build-info
|
||||||
./bin/build-server.sh
|
./bin/build-server.sh
|
||||||
- name: Prepare artifacts
|
- name: Prepare artifacts
|
||||||
if: runner.os != 'windows'
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p upload
|
mkdir -p upload
|
||||||
file=$(find dist -name '*.tar.xz' -print -quit)
|
file=$(find dist -name '*.tar.xz' -print -quit)
|
||||||
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
|
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: TriliumNextNotes linux server x64
|
name: TriliumNextNotes linux server ${{ matrix.arch }}
|
||||||
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
|
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
coverage/
|
||||||
src/public/app-dist/
|
src/public/app-dist/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
@ -2,21 +2,57 @@
|
|||||||
|
|
||||||
set -e # Fail on any command error
|
set -e # Fail on any command error
|
||||||
|
|
||||||
PKG_DIR=dist/trilium-linux-x64-server
|
# Debug output
|
||||||
|
echo "Matrix Arch: $MATRIX_ARCH"
|
||||||
|
|
||||||
|
# Detect architecture from matrix input, fallback to system architecture
|
||||||
|
if [ -n "$MATRIX_ARCH" ]; then
|
||||||
|
ARCH=$MATRIX_ARCH
|
||||||
|
else
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
# Convert system architecture to our naming convention
|
||||||
|
case $ARCH in
|
||||||
|
x86_64) ARCH="x64" ;;
|
||||||
|
aarch64) ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debug output
|
||||||
|
echo "Selected Arch: $ARCH"
|
||||||
|
|
||||||
|
# Set Node.js version and architecture-specific filename
|
||||||
NODE_VERSION=20.15.1
|
NODE_VERSION=20.15.1
|
||||||
|
NODE_ARCH=$ARCH
|
||||||
|
|
||||||
|
# Debug output
|
||||||
|
echo "Node arch: $NODE_ARCH"
|
||||||
|
|
||||||
|
# Special case for x64 in Node.js downloads
|
||||||
|
if [ "$NODE_ARCH" = "x64" ]; then
|
||||||
|
NODE_FILENAME="x64"
|
||||||
|
elif [ "$NODE_ARCH" = "arm64" ]; then
|
||||||
|
NODE_FILENAME="arm64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debug output
|
||||||
|
echo "Node filename: $NODE_FILENAME"
|
||||||
|
|
||||||
|
PKG_DIR=dist/trilium-linux-${ARCH}-server
|
||||||
|
echo "Package directory: $PKG_DIR"
|
||||||
|
|
||||||
if [ "$1" != "DONTCOPY" ]
|
if [ "$1" != "DONTCOPY" ]
|
||||||
then
|
then
|
||||||
./bin/copy-trilium.sh $PKG_DIR
|
# Need to modify copy-trilium.sh to accept the target directory
|
||||||
|
./bin/copy-trilium.sh "$PKG_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd dist
|
cd dist
|
||||||
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz
|
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||||
tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz
|
tar xfJ node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||||
rm node-v${NODE_VERSION}-linux-x64.tar.xz
|
rm node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
mv dist/node-v${NODE_VERSION}-linux-x64 $PKG_DIR/node
|
mv dist/node-v${NODE_VERSION}-linux-${NODE_FILENAME} $PKG_DIR/node
|
||||||
|
|
||||||
rm -r $PKG_DIR/node/lib/node_modules/npm
|
rm -r $PKG_DIR/node/lib/node_modules/npm
|
||||||
rm -r $PKG_DIR/node/include/node
|
rm -r $PKG_DIR/node/include/node
|
||||||
@ -37,4 +73,4 @@ VERSION=`jq -r ".version" package.json`
|
|||||||
|
|
||||||
cd dist
|
cd dist
|
||||||
|
|
||||||
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server
|
tar cJf trilium-linux-${ARCH}-server-${VERSION}.tar.xz trilium-linux-${ARCH}-server
|
||||||
|
@ -76,7 +76,6 @@ const copy = async () => {
|
|||||||
"node_modules/@excalidraw/excalidraw/dist/",
|
"node_modules/@excalidraw/excalidraw/dist/",
|
||||||
"node_modules/katex/dist/",
|
"node_modules/katex/dist/",
|
||||||
"node_modules/dayjs/",
|
"node_modules/dayjs/",
|
||||||
"node_modules/force-graph/dist/",
|
|
||||||
"node_modules/boxicons/css/",
|
"node_modules/boxicons/css/",
|
||||||
"node_modules/boxicons/fonts/",
|
"node_modules/boxicons/fonts/",
|
||||||
"node_modules/mermaid/dist/",
|
"node_modules/mermaid/dist/",
|
||||||
|
BIN
db/demo.zip
BIN
db/demo.zip
Binary file not shown.
@ -12,7 +12,7 @@ function getDataKey(password: any) {
|
|||||||
|
|
||||||
const encryptedDataKey = getOption("encryptedDataKey");
|
const encryptedDataKey = getOption("encryptedDataKey");
|
||||||
|
|
||||||
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
|
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey);
|
||||||
|
|
||||||
return decryptedDataKey;
|
return decryptedDataKey;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -16,7 +16,7 @@ function decryptString(dataKey: any, cipherText: any) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrypt(key: any, cipherText: any, ivLength = 13) {
|
function decrypt(key: any, cipherText: any) {
|
||||||
if (cipherText === null) {
|
if (cipherText === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -27,6 +27,8 @@ function decrypt(key: any, cipherText: any, ivLength = 13) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
|
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
|
||||||
|
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
|
||||||
|
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
|
||||||
const iv = cipherTextBufferWithIv.slice(0, ivLength);
|
const iv = cipherTextBufferWithIv.slice(0, ivLength);
|
||||||
|
|
||||||
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
||||||
|
12
dump-db/package-lock.json
generated
12
dump-db/package-lock.json
generated
@ -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"
|
||||||
|
27
e2e/help.spec.ts
Normal file
27
e2e/help.spec.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { test, expect, Page } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
test("Help popup", async ({ page, context }) => {
|
||||||
|
page.setDefaultTimeout(15_000);
|
||||||
|
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
const popupPromise = page.waitForEvent("popup");
|
||||||
|
await app.currentNoteSplit.press("F1");
|
||||||
|
await page.getByRole("link", { name: "online↗" }).click();
|
||||||
|
const popup = await popupPromise;
|
||||||
|
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Complete help in search", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
await app.launcherBar.locator(".bx-search").first().click();
|
||||||
|
await app.currentNoteSplit.locator(".search-settings .bx-help-circle").click();
|
||||||
|
const popupPromise = page.waitForEvent("popup");
|
||||||
|
await page.getByRole("link", { name: "complete help on search syntax" }).click();
|
||||||
|
const popup = await popupPromise;
|
||||||
|
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
|
||||||
|
});
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
});
|
}
|
||||||
|
22
e2e/note_types/mindmap.spec.ts
Normal file
22
e2e/note_types/mindmap.spec.ts
Normal 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();
|
||||||
|
});
|
9
e2e/note_types/note_map.spec.ts
Normal file
9
e2e/note_types/note_map.spec.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import App from "../support/app";
|
||||||
|
|
||||||
|
test("renders global map", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
await app.launcherBar.locator(".launcher-button.bx-map-alt").click();
|
||||||
|
await expect(app.currentNoteSplit.locator(".force-graph-container canvas")).toBeVisible();
|
||||||
|
});
|
@ -49,3 +49,20 @@ test("Highlights list is displayed", async ({ page, context }) => {
|
|||||||
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
|
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Displays math popup", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
await app.goToNoteInNewTab("Empty text");
|
||||||
|
const noteContent = app.currentNoteSplit.locator(".note-detail-editable-text-editor")
|
||||||
|
await noteContent.fill("Hello world");
|
||||||
|
await noteContent.press("ControlOrMeta+M");
|
||||||
|
|
||||||
|
const mathForm = page.locator(".ck-math-form");
|
||||||
|
await expect(mathForm).toBeVisible();
|
||||||
|
|
||||||
|
await mathForm.locator(".ck-input").first().fill("e=mc^2");
|
||||||
|
|
||||||
|
const preview = page.locator('[id^="math-preview"]');
|
||||||
|
await expect(preview).toMatchAriaSnapshot("- math: e = m c 2");
|
||||||
|
});
|
||||||
|
18
e2e/shared_notes.spec.ts
Normal file
18
e2e/shared_notes.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect, Page } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
test("Goes to share root", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto({ url: "/share" });
|
||||||
|
const noteTitle = "Shared notes";
|
||||||
|
await expect(page).toHaveTitle(noteTitle);
|
||||||
|
await expect(page.locator("h1")).toHaveText(noteTitle);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Goes to parent share root page", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto({ url: "/share/bKMn5EFv9KS2" });
|
||||||
|
await expect(page.locator("h1")).toHaveText("Child note");
|
||||||
|
await page.locator("#parentLink a").click();
|
||||||
|
await page.waitForURL("/share/");
|
||||||
|
});
|
@ -2,15 +2,19 @@ import { expect, Locator, Page } from "@playwright/test";
|
|||||||
import type { BrowserContext } from "@playwright/test";
|
import type { BrowserContext } from "@playwright/test";
|
||||||
|
|
||||||
interface GotoOpts {
|
interface GotoOpts {
|
||||||
|
url?: string;
|
||||||
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,26 +24,33 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
async goto(opts: GotoOpts = {}) {
|
async goto({ url, isMobile }: 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: isMobile ? "mobile" : "desktop"
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await this.page.goto("/", { waitUntil: "networkidle" });
|
if (!url) {
|
||||||
|
url = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.page.goto(url, { waitUntil: "networkidle" });
|
||||||
|
|
||||||
// Wait for the page to load.
|
// Wait for the page to load.
|
||||||
|
if (url === "/") {
|
||||||
await expect(this.page.locator(".tree"))
|
await expect(this.page.locator(".tree"))
|
||||||
.toContainText("Trilium Integration Test");
|
.toContainText("Trilium Integration Test");
|
||||||
await this.closeAllTabs();
|
await this.closeAllTabs();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async goToNoteInNewTab(noteTitle: string) {
|
async goToNoteInNewTab(noteTitle: string) {
|
||||||
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
|
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
|
||||||
@ -60,19 +71,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.
@ -1,23 +0,0 @@
|
|||||||
import test, { expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test("Help popup", async ({ page }) => {
|
|
||||||
await page.goto("http://localhost:8082");
|
|
||||||
await page.getByText("Trilium Integration Test DB").click();
|
|
||||||
|
|
||||||
await page.locator("body").press("F1");
|
|
||||||
await page.getByRole("link", { name: "online↗" }).click();
|
|
||||||
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Complete help in search", async ({ page }) => {
|
|
||||||
await page.goto("http://localhost:8082");
|
|
||||||
|
|
||||||
// Clear all tabs
|
|
||||||
await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" });
|
|
||||||
await page.getByText("Close all tabs").click();
|
|
||||||
|
|
||||||
await page.locator("#launcher-container").getByRole("button", { name: "" }).first().click();
|
|
||||||
await page.getByRole("cell", { name: " " }).locator("span").first().click();
|
|
||||||
await page.getByRole("button", { name: "complete help on search syntax" }).click();
|
|
||||||
expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
|
|
||||||
});
|
|
@ -1,17 +0,0 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
const ROOT_URL = "http://localhost:8080";
|
|
||||||
const LOGIN_PASSWORD = "eliandoran";
|
|
||||||
|
|
||||||
test("Can insert equations", async ({ page }) => {
|
|
||||||
await page.setDefaultTimeout(60_000);
|
|
||||||
await page.setDefaultNavigationTimeout(60_000);
|
|
||||||
|
|
||||||
// Create a new note
|
|
||||||
// await page.locator("button.button-widget.bx-file-blank")
|
|
||||||
// .click();
|
|
||||||
|
|
||||||
const activeNote = page.locator(".component.note-split:visible");
|
|
||||||
const noteContent = activeNote.locator(".note-detail-editable-text-editor");
|
|
||||||
await noteContent.press("Ctrl+M");
|
|
||||||
});
|
|
1
libraries/mermaid-elk/elk.min.js
vendored
1
libraries/mermaid-elk/elk.min.js
vendored
File diff suppressed because one or more lines are too long
13
libraries/mermaid-elk/package-lock.json
generated
vendored
13
libraries/mermaid-elk/package-lock.json
generated
vendored
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
13
libraries/mermaid-elk/package.json
vendored
13
libraries/mermaid-elk/package.json
vendored
@ -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": {}
|
|
||||||
}
|
|
19
libraries/mermaid-elk/webpack.config.cjs
vendored
19
libraries/mermaid-elk/webpack.config.cjs
vendored
@ -1,19 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const webpack = require("webpack");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: "production",
|
|
||||||
entry: "../../node_modules/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs",
|
|
||||||
output: {
|
|
||||||
library: "MERMAID_ELK",
|
|
||||||
filename: "elk.min.js",
|
|
||||||
path: path.resolve(__dirname),
|
|
||||||
libraryTarget: "umd",
|
|
||||||
libraryExport: "default"
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.optimize.LimitChunkCountPlugin({
|
|
||||||
maxChunks: 1
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
// Used to register the loader with Node.js
|
|
||||||
// This is used to avoid the warning message when using the loader
|
|
||||||
// Can be removed if this PR is merged:
|
|
||||||
// https://github.com/TypeStrong/ts-node/pull/2073
|
|
||||||
// Then probably can change webpack comand to
|
|
||||||
// "webpack": "cross-env NODE_OPTIONS=--import=ts-node/esm webpack -c webpack.config.ts",
|
|
||||||
|
|
||||||
import { register } from "node:module";
|
|
||||||
import { pathToFileURL } from "node:url";
|
|
||||||
register("ts-node/esm", pathToFileURL("./"));
|
|
3489
package-lock.json
generated
3489
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@ -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",
|
||||||
@ -61,11 +61,12 @@
|
|||||||
"@mermaid-js/layout-elk": "0.1.7",
|
"@mermaid-js/layout-elk": "0.1.7",
|
||||||
"@mind-elixir/node-menu": "1.0.3",
|
"@mind-elixir/node-menu": "1.0.3",
|
||||||
"@triliumnext/express-partial-content": "1.0.1",
|
"@triliumnext/express-partial-content": "1.0.1",
|
||||||
|
"@types/react-dom": "18.3.1",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"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 +75,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",
|
||||||
@ -88,8 +89,8 @@
|
|||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-rate-limit": "7.5.0",
|
"express-rate-limit": "7.5.0",
|
||||||
"express-session": "1.18.1",
|
"express-session": "1.18.1",
|
||||||
"force-graph": "1.47.2",
|
"force-graph": "1.49.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.3.0",
|
||||||
"helmet": "8.0.0",
|
"helmet": "8.0.0",
|
||||||
"html": "1.0.0",
|
"html": "1.0.0",
|
||||||
"html2plaintext": "2.1.4",
|
"html2plaintext": "2.1.4",
|
||||||
@ -109,13 +110,13 @@
|
|||||||
"jquery.fancytree": "2.38.4",
|
"jquery.fancytree": "2.38.4",
|
||||||
"jsdom": "26.0.0",
|
"jsdom": "26.0.0",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.19",
|
"katex": "0.16.21",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "15.0.6",
|
"marked": "15.0.6",
|
||||||
"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 +137,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 +152,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 +161,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 +175,8 @@
|
|||||||
"@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/react": "18.3.1",
|
||||||
"@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 +190,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 +201,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"
|
||||||
|
@ -41,10 +41,10 @@ export default defineConfig({
|
|||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
// {
|
||||||
name: 'firefox',
|
// name: 'firefox',
|
||||||
use: { ...devices['Desktop Firefox'] },
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
},
|
// },
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// name: 'webkit',
|
// name: 'webkit',
|
||||||
@ -73,9 +73,9 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: !process.env.TRILIUM_DOCKER ? {
|
||||||
command: 'npm run integration-mem-db-dev',
|
command: 'npm run integration-mem-db-dev',
|
||||||
url: SERVER_URL,
|
url: SERVER_URL,
|
||||||
// reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
} : undefined,
|
||||||
});
|
});
|
||||||
|
@ -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();
|
|
||||||
|
345
spec-es6/data_dir.spec.ts
Normal file
345
spec-es6/data_dir.spec.ts
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { getTriliumDataDir as getTriliumDataDirType, getDataDirs as getDataDirsType, getPlatformAppDataDir as getPlatformAppDataDirType } from "../src/services/data_dir";
|
||||||
|
|
||||||
|
describe("data_dir.ts unit tests", async () => {
|
||||||
|
let getTriliumDataDir: typeof getTriliumDataDirType;
|
||||||
|
let getPlatformAppDataDir: typeof getPlatformAppDataDirType;
|
||||||
|
let getDataDirs: typeof getDataDirsType;
|
||||||
|
|
||||||
|
const mockFn = {
|
||||||
|
existsSyncMock: vi.fn(),
|
||||||
|
mkdirSyncMock: vi.fn(),
|
||||||
|
osHomedirMock: vi.fn(),
|
||||||
|
osPlatformMock: vi.fn(),
|
||||||
|
pathJoinMock: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
// using doMock, to avoid hoisting, so that we can use the mockFn object
|
||||||
|
// to collect all mocked Fns
|
||||||
|
vi.doMock("node:fs", () => {
|
||||||
|
return {
|
||||||
|
default: {
|
||||||
|
existsSync: mockFn.existsSyncMock,
|
||||||
|
mkdirSync: mockFn.mkdirSyncMock
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.doMock("node:os", () => {
|
||||||
|
return {
|
||||||
|
default: {
|
||||||
|
homedir: mockFn.osHomedirMock,
|
||||||
|
platform: mockFn.osPlatformMock
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.doMock("path", () => {
|
||||||
|
return {
|
||||||
|
join: mockFn.pathJoinMock
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// import function to test now, after creating the mocks
|
||||||
|
({ getTriliumDataDir } = await import("../src/services/data_dir.ts"));
|
||||||
|
({ getPlatformAppDataDir } = await import("../src/services/data_dir.ts"));
|
||||||
|
({ getDataDirs } = await import("../src/services/data_dir.ts"));
|
||||||
|
|
||||||
|
// helper to reset call counts
|
||||||
|
const resetAllMocks = () => {
|
||||||
|
Object.values(mockFn).forEach((mockedFn) => {
|
||||||
|
mockedFn.mockReset();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// helper to set mocked Platform
|
||||||
|
const setMockPlatform = (osPlatform: string, homedir: string, pathJoin: string) => {
|
||||||
|
mockFn.osPlatformMock.mockImplementation(() => osPlatform);
|
||||||
|
mockFn.osHomedirMock.mockImplementation(() => homedir);
|
||||||
|
mockFn.pathJoinMock.mockImplementation(() => pathJoin);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("#getPlatformAppDataDir()", () => {
|
||||||
|
type TestCaseGetPlatformAppDataDir = [description: string, fnValue: Parameters<typeof getPlatformAppDataDir>, expectedValueFn: (val: ReturnType<typeof getPlatformAppDataDir>) => boolean];
|
||||||
|
const testCases: TestCaseGetPlatformAppDataDir[] = [
|
||||||
|
["w/ unsupported OS it should return 'null'", ["aix", undefined], (val) => val === null],
|
||||||
|
|
||||||
|
["w/ win32 and no APPDATA set it should return 'null'", ["win32", undefined], (val) => val === null],
|
||||||
|
|
||||||
|
["w/ win32 and set APPDATA it should return set 'APPDATA'", ["win32", "AppData"], (val) => val === "AppData"],
|
||||||
|
|
||||||
|
["w/ linux it should return '/.local/share'", ["linux", undefined], (val) => val !== null && val.endsWith("/.local/share")],
|
||||||
|
|
||||||
|
["w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share", ["linux", "FakeAppData"], (val) => val !== null && val.endsWith("/.local/share")],
|
||||||
|
|
||||||
|
["w/ darwin it should return /Library/Application Support", ["darwin", undefined], (val) => val !== null && val.endsWith("/Library/Application Support")]
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const [testDescription, value, isExpected] = testCase;
|
||||||
|
return it(testDescription, () => {
|
||||||
|
const actual = getPlatformAppDataDir(...value);
|
||||||
|
const result = isExpected(actual);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#getTriliumDataDir", async () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// make sure these are not set
|
||||||
|
delete process.env.TRILIUM_DATA_DIR;
|
||||||
|
delete process.env.APPDATA;
|
||||||
|
|
||||||
|
resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* case A – process.env.TRILIUM_DATA_DIR is set
|
||||||
|
* case B – process.env.TRILIUM_DATA_DIR is not set and Trilium folder is existing in platform
|
||||||
|
* case C – process.env.TRILIUM_DATA_DIR is not set and Trilium folder is not existing in platform's home dir
|
||||||
|
* case D – fallback to creating Trilium folder in home dir
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("case A", () => {
|
||||||
|
it("when folder exists – it should return the path, without attempting to create the folder", async () => {
|
||||||
|
const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A1";
|
||||||
|
process.env.TRILIUM_DATA_DIR = mockTriliumDataPath;
|
||||||
|
|
||||||
|
// set fs.existsSync to true, i.e. the folder does exist
|
||||||
|
mockFn.existsSyncMock.mockImplementation(() => true);
|
||||||
|
|
||||||
|
const result = getTriliumDataDir("trilium-data");
|
||||||
|
|
||||||
|
// createDirIfNotExisting should call existsync 1 time and mkdirSync 0 times -> as it does not need to create the folder
|
||||||
|
// and return value should be TRILIUM_DATA_DIR value from process.env
|
||||||
|
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(result).toEqual(process.env.TRILIUM_DATA_DIR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when folder does not exist – it should attempt to create the folder and return the path", async () => {
|
||||||
|
const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A2";
|
||||||
|
process.env.TRILIUM_DATA_DIR = mockTriliumDataPath;
|
||||||
|
|
||||||
|
// set fs.existsSync mock to return false, i.e. the folder does not exist
|
||||||
|
mockFn.existsSyncMock.mockImplementation(() => false);
|
||||||
|
|
||||||
|
const result = getTriliumDataDir("trilium-data");
|
||||||
|
|
||||||
|
// createDirIfNotExisting should call existsync 1 time and mkdirSync 1 times -> as it has to create the folder
|
||||||
|
// and return value should be TRILIUM_DATA_DIR value from process.env
|
||||||
|
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqual(process.env.TRILIUM_DATA_DIR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("case B", () => {
|
||||||
|
it("it should check if folder exists and return it", async () => {
|
||||||
|
const homedir = "/home/mock";
|
||||||
|
const dataDirName = "trilium-data";
|
||||||
|
const mockTriliumDataPath = `${homedir}/${dataDirName}`;
|
||||||
|
|
||||||
|
mockFn.pathJoinMock.mockImplementation(() => mockTriliumDataPath);
|
||||||
|
|
||||||
|
// set fs.existsSync to true, i.e. the folder does exist
|
||||||
|
mockFn.existsSyncMock.mockImplementation(() => true);
|
||||||
|
|
||||||
|
const result = getTriliumDataDir(dataDirName);
|
||||||
|
|
||||||
|
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqual(mockTriliumDataPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("case C", () => {
|
||||||
|
it("w/ Platform 'Linux', an existing App Data Folder (~/.local/share) but non-existing Trilium dir (~/.local/share/trilium-data) – it should attempt to create the dir", async () => {
|
||||||
|
const homedir = "/home/mock";
|
||||||
|
const dataDirName = "trilium-data";
|
||||||
|
const mockPlatformDataPath = `${homedir}/.local/share/${dataDirName}`;
|
||||||
|
|
||||||
|
// mock set: os.platform, os.homedir and pathJoin return values
|
||||||
|
setMockPlatform("linux", homedir, mockPlatformDataPath);
|
||||||
|
|
||||||
|
// use Generator to precisely control order of fs.existSync return values
|
||||||
|
const existsSyncMockGen = (function* () {
|
||||||
|
// 1) fs.existSync -> case B
|
||||||
|
yield false;
|
||||||
|
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
|
||||||
|
yield true;
|
||||||
|
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
|
||||||
|
yield false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||||
|
|
||||||
|
const result = getTriliumDataDir(dataDirName);
|
||||||
|
|
||||||
|
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqual(mockPlatformDataPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("w/ Platform Linux, an existing App Data Folder (~/.local/share) AND an existing Trilium Data dir – it should return path to the dir", async () => {
|
||||||
|
const homedir = "/home/mock";
|
||||||
|
const dataDirName = "trilium-data";
|
||||||
|
const mockPlatformDataPath = `${homedir}/.local/share/${dataDirName}`;
|
||||||
|
|
||||||
|
// mock set: os.platform, os.homedir and pathJoin return values
|
||||||
|
setMockPlatform("linux", homedir, mockPlatformDataPath);
|
||||||
|
|
||||||
|
// use Generator to precisely control order of fs.existSync return values
|
||||||
|
const existsSyncMockGen = (function* () {
|
||||||
|
// 1) fs.existSync -> case B
|
||||||
|
yield false;
|
||||||
|
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
|
||||||
|
yield true;
|
||||||
|
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
|
||||||
|
yield true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||||
|
|
||||||
|
const result = getTriliumDataDir(dataDirName);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPlatformDataPath);
|
||||||
|
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("w/ Platform 'win32' and set process.env.APPDATA behaviour", async () => {
|
||||||
|
const homedir = "C:\\Users\\mock";
|
||||||
|
const dataDirName = "trilium-data";
|
||||||
|
const appDataDir = `${homedir}\\AppData\\Roaming`;
|
||||||
|
const mockPlatformDataPath = `${appDataDir}\\${dataDirName}`;
|
||||||
|
process.env.APPDATA = `${appDataDir}`;
|
||||||
|
|
||||||
|
// mock set: os.platform, os.homedir and pathJoin return values
|
||||||
|
setMockPlatform("win32", homedir, mockPlatformDataPath);
|
||||||
|
|
||||||
|
// use Generator to precisely control order of fs.existSync return values
|
||||||
|
const existsSyncMockGen = (function* () {
|
||||||
|
// 1) fs.existSync -> case B
|
||||||
|
yield false;
|
||||||
|
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
|
||||||
|
yield true;
|
||||||
|
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
|
||||||
|
yield false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||||
|
|
||||||
|
const result = getTriliumDataDir(dataDirName);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPlatformDataPath);
|
||||||
|
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("case D", () => {
|
||||||
|
it("w/ unknown PlatformAppDataDir it should attempt to create the folder in the homefolder", async () => {
|
||||||
|
const homedir = "/home/mock";
|
||||||
|
const dataDirName = "trilium-data";
|
||||||
|
const mockPlatformDataPath = `${homedir}/${dataDirName}`;
|
||||||
|
|
||||||
|
setMockPlatform("aix", homedir, mockPlatformDataPath);
|
||||||
|
|
||||||
|
const existsSyncMockGen = (function* () {
|
||||||
|
// first fs.existSync -> case B -> checking if folder exists in home folder
|
||||||
|
yield false;
|
||||||
|
// second fs.existSync -> case D -> triggered by createDirIfNotExisting
|
||||||
|
yield false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||||
|
|
||||||
|
const result = getTriliumDataDir(dataDirName);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPlatformDataPath);
|
||||||
|
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// mock pathJoin implementation to just return mockDataDir
|
||||||
|
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR";
|
||||||
|
mockFn.pathJoinMock.mockImplementation(() => mockDataDir);
|
||||||
|
|
||||||
|
const result = getDataDirs(mockDataDir);
|
||||||
|
|
||||||
|
for (const key in result) {
|
||||||
|
expect(result[key].startsWith(mockDataDir)).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
mockFn.pathJoinMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,79 +0,0 @@
|
|||||||
export function describe(name: string, cb: () => any) {
|
|
||||||
console.log(`Running ${name}`);
|
|
||||||
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function it(name: string, cb: () => any) {
|
|
||||||
console.log(` Running ${name}`);
|
|
||||||
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
export function expect(val: any) {
|
|
||||||
return {
|
|
||||||
toEqual: (comparedVal: any) => {
|
|
||||||
const jsonVal = JSON.stringify(val);
|
|
||||||
const comparedJsonVal = JSON.stringify(comparedVal);
|
|
||||||
|
|
||||||
if (jsonVal !== comparedJsonVal) {
|
|
||||||
console.trace("toEqual check failed.");
|
|
||||||
console.error(`expected: ${comparedJsonVal}`);
|
|
||||||
console.error(`got: ${jsonVal}`);
|
|
||||||
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toBeTruthy: () => {
|
|
||||||
if (!val) {
|
|
||||||
console.trace("toBeTruthy failed.");
|
|
||||||
console.error(`expected: truthy value`);
|
|
||||||
console.error(`got: ${val}`);
|
|
||||||
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toBeFalsy: () => {
|
|
||||||
if (!!val) {
|
|
||||||
console.trace("toBeFalsy failed.");
|
|
||||||
console.error(`expected: null, false, undefined, 0 or empty string`);
|
|
||||||
console.error(`got: ${val}`);
|
|
||||||
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toThrow: (errorMessage: any) => {
|
|
||||||
try {
|
|
||||||
val();
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.message !== errorMessage) {
|
|
||||||
console.trace("toThrow caught exception, but messages differ");
|
|
||||||
console.error(`expected: ${errorMessage}`);
|
|
||||||
console.error(`got: ${e.message}`);
|
|
||||||
console.error(`${e.stack}`);
|
|
||||||
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.trace("toThrow did not catch any exception.");
|
|
||||||
console.error(`expected: ${errorMessage}`);
|
|
||||||
console.error(`got: [none]`);
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function execute() {
|
|
||||||
console.log("");
|
|
||||||
|
|
||||||
if (errorCount) {
|
|
||||||
console.log(`!!!${errorCount} tests failed!!!`);
|
|
||||||
} else {
|
|
||||||
console.log("All tests passed!");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
|
import { expect, describe, it } from "vitest";
|
||||||
import sanitizeAttributeName from "../src/services/sanitize_attribute_name";
|
import sanitizeAttributeName from "../src/services/sanitize_attribute_name";
|
||||||
import { describe, it, execute, expect } from "./mini_test";
|
|
||||||
|
|
||||||
// fn value, expected value
|
// fn value, expected value
|
||||||
const testCases: [fnValue: string, expectedValue: string][] = [
|
const testCases: [fnValue: string, expectedValue: string][] = [
|
||||||
@ -31,9 +31,7 @@ describe("sanitizeAttributeName unit tests", () => {
|
|||||||
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
|
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
|
||||||
const [value, expected] = testCase;
|
const [value, expected] = testCase;
|
||||||
const actual = sanitizeAttributeName(value);
|
const actual = sanitizeAttributeName(value);
|
||||||
expect(actual).toEqual(expected);
|
expect(actual).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
execute();
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import { expect, describe, it } from "vitest";
|
||||||
import { formatDownloadTitle } from "../../src/services/utils.ts";
|
import { formatDownloadTitle } from "../../src/services/utils.ts";
|
||||||
import { describe, it, execute, expect } from "../mini_test.ts";
|
|
||||||
|
|
||||||
const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
|
const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
|
||||||
// empty fileName tests
|
// empty fileName tests
|
||||||
@ -55,9 +55,7 @@ describe("utils/formatDownloadTitle unit tests", () => {
|
|||||||
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
|
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
|
||||||
const [value, expected] = testCase;
|
const [value, expected] = testCase;
|
||||||
const actual = formatDownloadTitle(...value);
|
const actual = formatDownloadTitle(...value);
|
||||||
expect(actual).toEqual(expected);
|
expect(actual).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
execute();
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
describe("Notes", () => {
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
|
describe.todo("Notes", () => {
|
||||||
it("zzz", () => {});
|
it("zzz", () => {});
|
||||||
});
|
});
|
||||||
|
@ -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" });
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import lex from "../../src/services/search/services/lex.js";
|
import lex from "../../src/services/search/services/lex.js";
|
||||||
|
|
||||||
describe("Lexer fulltext", () => {
|
describe("Lexer fulltext", () => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import handleParens from "../../src/services/search/services/handle_parens.js";
|
import handleParens from "../../src/services/search/services/handle_parens.js";
|
||||||
import type { TokenStructure } from "../../src/services/search/services/types.js";
|
import type { TokenStructure } from "../../src/services/search/services/types.js";
|
||||||
|
|
||||||
|
@ -1,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";
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, beforeEach, } from "vitest";
|
||||||
import searchService from "../../src/services/search/services/search.js";
|
import searchService from "../../src/services/search/services/search.js";
|
||||||
import BNote from "../../src/becca/entities/bnote.js";
|
import BNote from "../../src/becca/entities/bnote.js";
|
||||||
import BBranch from "../../src/becca/entities/bbranch.js";
|
import BBranch from "../../src/becca/entities/bbranch.js";
|
||||||
@ -21,7 +22,7 @@ describe("Search", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("simple path match", () => {
|
it.skip("simple path match", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -31,7 +32,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("normal search looks also at attributes", () => {
|
it.skip("normal search looks also at attributes", () => {
|
||||||
const austria = becca_mocking.note("Austria");
|
const austria = becca_mocking.note("Austria");
|
||||||
const vienna = becca_mocking.note("Vienna");
|
const vienna = becca_mocking.note("Vienna");
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("normal search looks also at type and mime", () => {
|
it.skip("normal search looks also at type and mime", () => {
|
||||||
rootNote.child(becca_mocking.note("Effective Java", { type: "book", mime: "" })).child(becca_mocking.note("Hello World.java", { type: "code", mime: "text/x-java" }));
|
rootNote.child(becca_mocking.note("Effective Java", { type: "book", mime: "" })).child(becca_mocking.note("Hello World.java", { type: "code", mime: "text/x-java" }));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -68,7 +69,7 @@ describe("Search", () => {
|
|||||||
expect(searchResults.length).toEqual(2);
|
expect(searchResults.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("only end leafs are results", () => {
|
it.skip("only end leafs are results", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -78,7 +79,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("only end leafs are results", () => {
|
it.skip("only end leafs are results", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")));
|
rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -133,7 +134,7 @@ describe("Search", () => {
|
|||||||
expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("inherited label comparison", () => {
|
it.skip("inherited label comparison", () => {
|
||||||
rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic")));
|
rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic")));
|
||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
@ -183,8 +184,7 @@ describe("Search", () => {
|
|||||||
|
|
||||||
function test(query: string, expectedResultCount: number) {
|
function test(query: string, expectedResultCount: number) {
|
||||||
const searchResults = searchService.findResultsWithQuery(query, searchContext);
|
const searchResults = searchService.findResultsWithQuery(query, searchContext);
|
||||||
expect(searchResults.length)
|
expect(searchResults.length, `Searching for '${query}' unexpectedly returned ${Number(searchResults?.length)} instead of ${expectedResultCount} results. SearchResult: '${JSON.stringify(searchResults)}'`)
|
||||||
.withContext(`While searching for ${query} got unexpected result: [${searchResults.join(", ")}]`)
|
|
||||||
.toEqual(expectedResultCount);
|
.toEqual(expectedResultCount);
|
||||||
|
|
||||||
if (expectedResultCount === 1) {
|
if (expectedResultCount === 1) {
|
||||||
@ -549,7 +549,7 @@ describe("Search", () => {
|
|||||||
expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
|
expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("test note.text *=* something", () => {
|
it.skip("test note.text *=* something", () => {
|
||||||
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
||||||
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
||||||
|
|
||||||
@ -562,7 +562,7 @@ describe("Search", () => {
|
|||||||
expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
|
expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("test that fulltext does not match archived notes", () => {
|
it.skip("test that fulltext does not match archived notes", () => {
|
||||||
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
const italy = becca_mocking.note("Italy").label("capital", "Rome");
|
||||||
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
import becca_mocking from "./becca_mocking.js";
|
import becca_mocking from "./becca_mocking.js";
|
||||||
import ValueExtractor from "../../src/services/search/value_extractor.js";
|
import ValueExtractor from "../../src/services/search/value_extractor.js";
|
||||||
import becca from "../../src/becca/becca.js";
|
import becca from "../../src/becca/becca.js";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import child_process from "child_process";
|
import type child_process from "child_process";
|
||||||
|
|
||||||
let etapiAuthToken: string | undefined;
|
let etapiAuthToken: string | undefined;
|
||||||
|
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"spec_dir": "",
|
|
||||||
"spec_files": [
|
|
||||||
"spec/**/*.spec.ts",
|
|
||||||
"src/**/*.spec.ts"
|
|
||||||
],
|
|
||||||
"helpers": ["helpers/**/*.js"],
|
|
||||||
"stopSpecOnExpectationFailure": false,
|
|
||||||
"random": true
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
import { trimIndentation } from "./utils.js";
|
import { trimIndentation } from "./utils.js";
|
||||||
|
|
||||||
describe("Utils", () => {
|
describe("Utils", () => {
|
||||||
|
@ -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;
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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";
|
||||||
@ -1528,7 +1528,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isLaunchBarConfig() {
|
isLaunchBarConfig() {
|
||||||
return this.type === "launcher" || ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId);
|
return this.type === "launcher"
|
||||||
|
|| ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId)
|
||||||
|
|| ["_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(this.noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
isOptions() {
|
isOptions() {
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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";
|
||||||
|
@ -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 {
|
||||||
|
@ -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";
|
||||||
|
@ -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";
|
||||||
|
@ -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.`);
|
||||||
|
@ -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;
|
||||||
@ -108,6 +108,7 @@ export type CommandMappings = {
|
|||||||
toggleNoteHoisting: ContextMenuCommandData;
|
toggleNoteHoisting: ContextMenuCommandData;
|
||||||
insertNoteAfter: ContextMenuCommandData;
|
insertNoteAfter: ContextMenuCommandData;
|
||||||
insertChildNote: ContextMenuCommandData;
|
insertChildNote: ContextMenuCommandData;
|
||||||
|
delete: ContextMenuCommandData;
|
||||||
protectSubtree: ContextMenuCommandData;
|
protectSubtree: ContextMenuCommandData;
|
||||||
unprotectSubtree: ContextMenuCommandData;
|
unprotectSubtree: ContextMenuCommandData;
|
||||||
openBulkActionsDialog: ContextMenuCommandData;
|
openBulkActionsDialog: ContextMenuCommandData;
|
||||||
@ -262,6 +263,9 @@ type EventMappings = {
|
|||||||
};
|
};
|
||||||
noteContextRemovedEvent: {
|
noteContextRemovedEvent: {
|
||||||
ntxIds: string[];
|
ntxIds: string[];
|
||||||
|
};
|
||||||
|
exportSvg: {
|
||||||
|
ntxId: string;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -274,15 +278,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 +380,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
@ -515,10 +515,10 @@ class FNote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param [name] - label name to filter
|
* @param name - label name to filter
|
||||||
* @returns all note's labels (attributes with type label), including inherited ones
|
* @returns all note's labels (attributes with type label), including inherited ones
|
||||||
*/
|
*/
|
||||||
getOwnedLabels(name: string) {
|
getOwnedLabels(name?: string) {
|
||||||
return this.getOwnedAttributes(LABEL, name);
|
return this.getOwnedAttributes(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ interface ContextMenuOptions<T extends CommandNames> {
|
|||||||
orientation?: "left";
|
orientation?: "left";
|
||||||
selectMenuItemHandler: MenuHandler<T>;
|
selectMenuItemHandler: MenuHandler<T>;
|
||||||
items: MenuItem<T>[];
|
items: MenuItem<T>[];
|
||||||
|
/** On mobile, if set to `true` then the context menu is shown near the element. If `false` (default), then the context menu is shown at the bottom of the screen. */
|
||||||
|
forcePositionOnMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuSeparatorItem {
|
interface MenuSeparatorItem {
|
||||||
@ -60,6 +62,7 @@ class ContextMenu {
|
|||||||
await this.hide();
|
await this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
|
||||||
this.$cover.addClass("show");
|
this.$cover.addClass("show");
|
||||||
$("body").addClass("context-menu-shown");
|
$("body").addClass("context-menu-shown");
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
@ -34,8 +34,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
|
|||||||
|
|
||||||
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
|
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
|
||||||
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
|
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
|
||||||
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers";
|
const isVisibleItem = (parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers");
|
||||||
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers";
|
const isAvailableItem = (parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers");
|
||||||
const isItem = isVisibleItem || isAvailableItem;
|
const isItem = isVisibleItem || isAvailableItem;
|
||||||
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
|
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
|
||||||
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
|
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
|
||||||
@ -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>) {
|
||||||
|
@ -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,
|
||||||
|
@ -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>) {
|
||||||
|
@ -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;
|
||||||
|
@ -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)` : "";
|
||||||
|
@ -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 = "") {
|
||||||
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
@ -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>;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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", {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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> = {};
|
||||||
|
|
||||||
|
@ -68,26 +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 = {
|
|
||||||
js: ["node_modules/react/umd/react.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"]
|
|
||||||
};
|
|
||||||
|
|
||||||
const MARKJS: Library = {
|
const MARKJS: Library = {
|
||||||
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
|
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
|
||||||
};
|
};
|
||||||
@ -96,10 +80,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 +193,8 @@ export default {
|
|||||||
CALENDAR_WIDGET,
|
CALENDAR_WIDGET,
|
||||||
KATEX,
|
KATEX,
|
||||||
WHEEL_ZOOM,
|
WHEEL_ZOOM,
|
||||||
FORCE_GRAPH,
|
|
||||||
MERMAID,
|
MERMAID,
|
||||||
MERMAID_ELK,
|
|
||||||
EXCALIDRAW,
|
|
||||||
MARKJS,
|
MARKJS,
|
||||||
I18NEXT,
|
I18NEXT,
|
||||||
MIND_ELIXIR,
|
|
||||||
HIGHLIGHT_JS
|
HIGHLIGHT_JS
|
||||||
};
|
};
|
||||||
|
@ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
|
|||||||
return goToLinkExt(evt, hrefLink, $link);
|
return goToLinkExt(evt, hrefLink, $link);
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement>) {
|
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link: JQuery<HTMLElement> | null) {
|
||||||
if (hrefLink?.startsWith("data:")) {
|
if (hrefLink?.startsWith("data:")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -242,20 +242,17 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
|
||||||
if (hrefLink?.startsWith("#fn")) {
|
if (hrefLink?.startsWith("#fn") && $link) {
|
||||||
return handleFootnote(hrefLink, $link);
|
return handleFootnote(hrefLink, $link);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
|
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
|
||||||
|
|
||||||
const ctrlKey = utils.isCtrlKey(evt);
|
const ctrlKey = utils.isCtrlKey(evt);
|
||||||
const isLeftClick = evt.which === 1;
|
const isLeftClick = ("which" in evt && evt.which === 1);
|
||||||
const isMiddleClick = evt.which === 2;
|
const isMiddleClick = ("which" in evt && evt.which === 2);
|
||||||
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
|
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
|
||||||
|
|
||||||
const leftClick = evt.which === 1;
|
|
||||||
const middleClick = evt.which === 2;
|
|
||||||
|
|
||||||
if (notePath) {
|
if (notePath) {
|
||||||
if (openInNewTab) {
|
if (openInNewTab) {
|
||||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
|
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
|
||||||
@ -276,7 +273,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
|
|||||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||||
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
|
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
|
||||||
|
|
||||||
if (openInNewTab || (withinEditLink && (leftClick || middleClick)) || (outsideOfCKEditor && (leftClick || middleClick))) {
|
if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
|
||||||
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
|
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
|
||||||
window.open(hrefLink, "_blank");
|
window.open(hrefLink, "_blank");
|
||||||
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
|
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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">
|
||||||
|
@ -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() {
|
||||||
|
@ -36,11 +36,15 @@ class Options {
|
|||||||
|
|
||||||
getInt(key: string) {
|
getInt(key: string) {
|
||||||
const value = this.arr?.[key];
|
const value = this.arr?.[key];
|
||||||
if (typeof value !== "string") {
|
if (typeof value === "number") {
|
||||||
return null;
|
return value;
|
||||||
}
|
}
|
||||||
|
if (typeof value == "string") {
|
||||||
return parseInt(value);
|
return parseInt(value);
|
||||||
}
|
}
|
||||||
|
console.warn("Attempting to read int for unsupported value: ", value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
getFloat(key: string) {
|
getFloat(key: string) {
|
||||||
const value = this.arr?.[key];
|
const value = this.arr?.[key];
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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");
|
||||||
|
@ -43,6 +43,14 @@ export default class SpacedUpdate {
|
|||||||
return allSaved;
|
return allSaved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally {@link scheduleUpdate()} would actually trigger the update only once per {@link updateInterval}. If the method is called 200 times within 20s, it will execute only 20 times.
|
||||||
|
* Sometimes, if the updates are continuous this would cause a performance impact. Resetting the time ensures that the calls to {@link triggerUpdate} have stopped before actually triggering an update.
|
||||||
|
*/
|
||||||
|
resetUpdateTimer() {
|
||||||
|
this.lastUpdated = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
triggerUpdate() {
|
triggerUpdate() {
|
||||||
if (!this.changed) {
|
if (!this.changed) {
|
||||||
return;
|
return;
|
||||||
|
@ -97,7 +97,7 @@ function isMac() {
|
|||||||
return navigator.platform.indexOf("Mac") > -1;
|
return navigator.platform.indexOf("Mac") > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
|
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) {
|
||||||
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
src/public/app/types-lib.d.ts
vendored
4
src/public/app/types-lib.d.ts
vendored
@ -20,3 +20,7 @@ declare module "draggabilly" {
|
|||||||
destroy();
|
destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@mind-elixir/node-menu' {
|
||||||
|
export default mindmap;
|
||||||
|
}
|
||||||
|
5
src/public/app/types.d.ts
vendored
5
src/public/app/types.d.ts
vendored
@ -155,13 +155,12 @@ declare global {
|
|||||||
registerLayoutLoaders(loader: MermaidLoader);
|
registerLayoutLoaders(loader: MermaidLoader);
|
||||||
parse(content: string, opts: {
|
parse(content: string, opts: {
|
||||||
suppressErrors: true
|
suppressErrors: true
|
||||||
}): {
|
}): Promise<{
|
||||||
config: {
|
config: {
|
||||||
layout: string;
|
layout: string;
|
||||||
}
|
}
|
||||||
}
|
}>
|
||||||
};
|
};
|
||||||
var MERMAID_ELK: MermaidLoader;
|
|
||||||
|
|
||||||
var CKEditor: {
|
var CKEditor: {
|
||||||
BalloonEditor: {
|
BalloonEditor: {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -86,7 +86,7 @@ const TPL = `
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
margin-right: 5px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .global-menu .dropdown-submenu .dropdown-menu {
|
body.mobile .global-menu .dropdown-submenu .dropdown-menu {
|
||||||
@ -162,8 +162,7 @@ const TPL = `
|
|||||||
|
|
||||||
<li class="dropdown-item dropdown-submenu">
|
<li class="dropdown-item dropdown-submenu">
|
||||||
<span class="dropdown-toggle">
|
<span class="dropdown-toggle">
|
||||||
<span class="bx bx-chip"></span>
|
<span class="bx bx-chip"></span>${t("global_menu.advanced")}
|
||||||
${t("global_menu.advanced")}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user