Compare commits
274 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2b6504d9d | |||
| ab9b5a6926 | |||
| 687be6d722 | |||
| eaf548f056 | |||
| f1cb95d95e | |||
| 189a2296c4 | |||
| 3f6dc717b6 | |||
| aecba70efc | |||
| 546274a79d | |||
| 15c63f52dc | |||
| 55ce673f1b | |||
| 10536adecf | |||
| 7eed037ace | |||
| 7bac28bff4 | |||
| 939c2b7ef6 | |||
| e614e775fa | |||
| 4c30853258 | |||
| 144a7ff3a1 | |||
| 24026ff013 | |||
| 7d8e96d6df | |||
| 9125aeca29 | |||
| 4264e37256 | |||
| 21539857c3 | |||
| 1d9f15b9a2 | |||
| be115e82b6 | |||
| 3dd5623f73 | |||
| 1d9c43f77e | |||
| a234234373 | |||
| ad02b86719 | |||
| 74f6ad46ae | |||
| 3ed9528495 | |||
| c6730c5467 | |||
| 47fb4d7b27 | |||
| 20584f622d | |||
| 32fc5def77 | |||
| befdade7ba | |||
| 4492c60aa9 | |||
| 3f7bda6289 | |||
| 8e62c942ea | |||
| 61d4d3fd2c | |||
| e4b186ce8a | |||
| 7ae7831a27 | |||
| 189dfdb5cd | |||
| af204d0d74 | |||
| faf14e1b14 | |||
| e1a095d111 | |||
| 0d82b64ed1 | |||
| 395aee5657 | |||
| 20f2fa3135 | |||
| 24d4375552 | |||
| bc901a1cf6 | |||
| f3219da749 | |||
| 2031029076 | |||
| 577c61b0d1 | |||
| b7f8f00a14 | |||
| 2d86d9a7c0 | |||
| 8472e44fe6 | |||
| 2c2a404056 | |||
| 37cdb2fe5e | |||
| ec6b5cda80 | |||
| 3cfdc6dac7 | |||
| d35bdfa402 | |||
| c56eb9a8a1 | |||
| 9cca9fe074 | |||
| 5c7c995504 | |||
| 3fc9a10ab6 | |||
| 8778ca5f6c | |||
| 85c6764864 | |||
| c7bd5e2c67 | |||
| a313b96ff8 | |||
| edeaca6d1f | |||
| 279aa156cf | |||
| c0327bf8e2 | |||
| f672747cfc | |||
| 8150e394a4 | |||
| 25dcc35b3b | |||
| 0b0fcc0685 | |||
| 4203a1160e | |||
| 16b5eef650 | |||
| 9e2c592383 | |||
| a8e2c2901b | |||
| 7a3a5141af | |||
| 474ae481b6 | |||
| 0288ebcad9 | |||
| d814a4d49f | |||
| 5a40d3f020 | |||
| e06db0038f | |||
| 9b1279ce14 | |||
| 47b02da021 | |||
| 2e1ad24584 | |||
| dbd38ecedf | |||
| cd22102dab | |||
| ac262228f0 | |||
| 65553250b5 | |||
| 31491b957b | |||
| b813b107a8 | |||
| dc7dd51913 | |||
| be4ee4c173 | |||
| c2cb07ed08 | |||
| d1aa0e5f50 | |||
| 259dcdb568 | |||
| 6b906a91d7 | |||
| 08722d5935 | |||
| 1be3492f67 | |||
| 016a9e4a99 | |||
| 087d4790f4 | |||
| b2a5f06646 | |||
| 04367de112 | |||
| 3281bb8e9f | |||
| fed0598b47 | |||
| 05b433d44e | |||
| 2296d1a6ba | |||
| 1de9bc7c6f | |||
| e1c949aa10 | |||
| ba91082115 | |||
| ef5b2d60f3 | |||
| 1d03818337 | |||
| 986a1c25be | |||
| f76b454d5a | |||
| a3f257f3c5 | |||
| 2582924046 | |||
| 3206a7fe73 | |||
| f803b9f822 | |||
| 4d5e04fc5a | |||
| f66f437c8e | |||
| 5cefd4f50a | |||
| 8f09b6a192 | |||
| 278870e06a | |||
| d2c2d945aa | |||
| 2b8ee31be3 | |||
| ca8146413a | |||
| eca3955dc2 | |||
| 94a0403981 | |||
| e1952fe6b8 | |||
| 4be675c4e1 | |||
| 4e59f58ce6 | |||
| 6a0edb68de | |||
| 91ae4b629e | |||
| 815929c376 | |||
| 1405e22f89 | |||
| 7f15f8a7de | |||
| b47dc13ff1 | |||
| 9820e8aa12 | |||
| fb0dda982f | |||
| 0292489001 | |||
| 55ce7aac10 | |||
| f36ec9b018 | |||
| 632cfb3b7f | |||
| 24475255d9 | |||
| b913e4e090 | |||
| d56752e5a9 | |||
| 5ddd88ee55 | |||
| 66ad3779df | |||
| 6443b2ea66 | |||
| 9025516806 | |||
| c9619e1a1b | |||
| 121e7b33ec | |||
| 67ee3c30fb | |||
| 09e0805d16 | |||
| 627b2faf0b | |||
| f803bd6140 | |||
| 49246ee456 | |||
| 0ccf91721d | |||
| 658317799d | |||
| 5899ba9a57 | |||
| ef1ab56f51 | |||
| 637d1df0f5 | |||
| 4e9aafc919 | |||
| 0a0707434e | |||
| 1369cd8b16 | |||
| b367862fe8 | |||
| 3090233078 | |||
| 981e105de8 | |||
| c5d4df11a5 | |||
| cbc10e1f15 | |||
| dba0ef4945 | |||
| 31fb8eb3d3 | |||
| c7e688b6e6 | |||
| 8e3faffd35 | |||
| 148e166f5e | |||
| 0ae5453717 | |||
| 0a611ac4bb | |||
| 54654322e2 | |||
| eaacdb57d1 | |||
| f2382e2ef5 | |||
| 7d3f506efb | |||
| ab65913e52 | |||
| 134c056667 | |||
| 02250a842f | |||
| ac30e00c02 | |||
| 0776f17ebb | |||
| 3db93cdf24 | |||
| 043d92a1ab | |||
| 5985519570 | |||
| 7c7fd044c6 | |||
| 2167948509 | |||
| 7cf1fbf088 | |||
| e3bcd75a5a | |||
| 229f473f8b | |||
| 11009a5af8 | |||
| fdbc33aa23 | |||
| d9335bec37 | |||
| 47335062a4 | |||
| fd2779383d | |||
| 59d8def2c5 | |||
| 6ea8a31a22 | |||
| f96ab6da57 | |||
| b376f090d8 | |||
| b4ff765c5e | |||
| 423bea923c | |||
| 7e30ab2e06 | |||
| 6452fa123c | |||
| d19e9b4681 | |||
| 3f92966976 | |||
| fec0ae62b7 | |||
| 7fc5f9ab83 | |||
| 33baf6c917 | |||
| 6401f35783 | |||
| 4dfe2b4935 | |||
| dd3397bcbb | |||
| 8b34c149cc | |||
| fb47ecd747 | |||
| 9f148750d4 | |||
| 853a7abc4f | |||
| 8faff118b1 | |||
| 33274ada65 | |||
| 6be7109ce3 | |||
| 0ed06e76c3 | |||
| 186f17ec3e | |||
| 871ee7989e | |||
| 3f584af171 | |||
| 325a3b6c83 | |||
| e7eb385b8f | |||
| 8d026c8134 | |||
| 7e40200436 | |||
| 0cee8f9f20 | |||
| 32c4d7350c | |||
| af1d6540bc | |||
| 54bf2ad9ff | |||
| 751adf474f | |||
| a1b660cfcd | |||
| 66c367fe21 | |||
| 3abe5a348f | |||
| 467852191d | |||
| 9382c278b3 | |||
| 0e33395c47 | |||
| 00bdcfa803 | |||
| 331dae4eb0 | |||
| 748e30b63c | |||
| 76a0bffcc3 | |||
| b8f15d2fe3 | |||
| b2e1a3e97a | |||
| 5f605b3a91 | |||
| ec19ccd7a7 | |||
| 283a12b0d5 | |||
| 139bf3dcdf | |||
| 6dd8ab31d5 | |||
| e3d89ce2a5 | |||
| edc6b983ac | |||
| d684440c1f | |||
| 1e182f5820 | |||
| c8b745bc6a | |||
| d1be673763 | |||
| 2fbfc9d668 | |||
| 5ea3e67dc3 | |||
| 4b7445be8e | |||
| a9570965f1 | |||
| b77f8aeb43 | |||
| 01b88c52ef | |||
| 4c451753bc | |||
| 7076c4cbd6 | |||
| 9d32cd36ee | |||
| 0eab68e8d1 | |||
| 9b765b458c |
@@ -1,2 +1,3 @@
|
||||
package-lock.json linguist-generated=true
|
||||
**/package-lock.json linguist-generated=true
|
||||
**/package-lock.json linguist-generated=true
|
||||
libraries/** linguist-vendored
|
||||
@@ -72,9 +72,18 @@ jobs:
|
||||
with:
|
||||
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }}.${{matrix.os.extension}}
|
||||
path: upload/*.${{ matrix.os.extension }}
|
||||
build_linux_server-x64:
|
||||
name: Build Linux Server x86_64
|
||||
runs-on: ubuntu-latest
|
||||
build_linux_server:
|
||||
name: Build Linux Server
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
include:
|
||||
- arch: x64
|
||||
runs-on: ubuntu-latest
|
||||
- arch: arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up node & dependencies
|
||||
@@ -84,17 +93,18 @@ jobs:
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Linux server build (x86_64)
|
||||
- name: Run Linux server build
|
||||
env:
|
||||
MATRIX_ARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
npm run update-build-info
|
||||
./bin/build-server.sh
|
||||
- name: Prepare artifacts
|
||||
if: runner.os != 'windows'
|
||||
run: |
|
||||
mkdir -p upload
|
||||
file=$(find dist -name '*.tar.xz' -print -quit)
|
||||
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
|
||||
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: TriliumNextNotes linux server x64
|
||||
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
|
||||
name: TriliumNextNotes linux server ${{ matrix.arch }}
|
||||
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
|
||||
|
||||
@@ -92,7 +92,16 @@ jobs:
|
||||
asset_content_type: application/zip # required by GitHub API
|
||||
nightly-server:
|
||||
name: Deploy server nightly
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
include:
|
||||
- arch: x64
|
||||
runs-on: ubuntu-latest
|
||||
- arch: arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up node & dependencies
|
||||
@@ -102,22 +111,21 @@ jobs:
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Linux server build (x86_64)
|
||||
- name: Run Linux server build
|
||||
env:
|
||||
MATRIX_ARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
npm run update-build-info
|
||||
npm run ci-update-nightly-version
|
||||
./bin/build-server.sh
|
||||
- name: Prepare artifacts
|
||||
if: runner.os != 'windows'
|
||||
run: |
|
||||
mkdir -p upload
|
||||
file=$(find dist -name '*.tar.xz' -print -quit)
|
||||
cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
|
||||
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: TriliumNextNotes linux server x64
|
||||
path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
|
||||
overwrite: true
|
||||
name: TriliumNextNotes linux server ${{ matrix.arch }}
|
||||
path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
|
||||
|
||||
- name: Deploy release
|
||||
uses: WebFreak001/deploy-nightly@v3.2.0
|
||||
|
||||
@@ -66,8 +66,17 @@ jobs:
|
||||
fail_on_unmatched_files: true
|
||||
files: upload/*.*
|
||||
build_linux_server-x64:
|
||||
name: Build Linux Server x86_64
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Linux Server
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
include:
|
||||
- arch: x64
|
||||
runs-on: ubuntu-latest
|
||||
- arch: arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up node & dependencies
|
||||
@@ -77,16 +86,17 @@ jobs:
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Linux server build (x86_64)
|
||||
- name: Run Linux server build
|
||||
env:
|
||||
MATRIX_ARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
npm run update-build-info
|
||||
./bin/build-server.sh
|
||||
- name: Prepare artifacts
|
||||
if: runner.os != 'windows'
|
||||
run: |
|
||||
mkdir -p upload
|
||||
file=$(find dist -name '*.tar.xz' -print -quit)
|
||||
cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-server-linux-x64.tar.xz"
|
||||
cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
src/public/app-dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build stage
|
||||
FROM node:22.13.0-bullseye-slim AS builder
|
||||
FROM node:22.13.1-bullseye-slim AS builder
|
||||
|
||||
# Configure build dependencies in a single layer
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
@@ -28,7 +28,6 @@ RUN cp -R build/src/* src/. && \
|
||||
npm run webpack && \
|
||||
npm prune --omit=dev && \
|
||||
npm cache clean --force && \
|
||||
cp src/public/app/share.js src/public/app-dist/. && \
|
||||
cp -r src/public/app/doc_notes src/public/app-dist/. && \
|
||||
rm -rf src/public/app/* && \
|
||||
mkdir -p src/public/app/services && \
|
||||
@@ -37,7 +36,7 @@ RUN cp -R build/src/* src/. && \
|
||||
rm -r build
|
||||
|
||||
# Runtime stage
|
||||
FROM node:22.13.0-bullseye-slim
|
||||
FROM node:22.13.1-bullseye-slim
|
||||
|
||||
# Install only runtime dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build stage
|
||||
FROM node:22.13.0-alpine AS builder
|
||||
FROM node:22.13.1-alpine AS builder
|
||||
|
||||
# Configure build dependencies
|
||||
RUN apk add --no-cache --virtual .build-dependencies \
|
||||
@@ -27,7 +27,6 @@ RUN cp -R build/src/* src/. && \
|
||||
npm run webpack && \
|
||||
npm prune --omit=dev && \
|
||||
npm cache clean --force && \
|
||||
cp src/public/app/share.js src/public/app-dist/. && \
|
||||
cp -r src/public/app/doc_notes src/public/app-dist/. && \
|
||||
rm -rf src/public/app && \
|
||||
mkdir -p src/public/app/services && \
|
||||
@@ -36,7 +35,7 @@ RUN cp -R build/src/* src/. && \
|
||||
rm -r build
|
||||
|
||||
# Runtime stage
|
||||
FROM node:22.13.0-alpine
|
||||
FROM node:22.13.1-alpine
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
@@ -2,21 +2,57 @@
|
||||
|
||||
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_ARCH=$ARCH
|
||||
|
||||
# Debug output
|
||||
echo "Node arch: $NODE_ARCH"
|
||||
|
||||
# Special case for x64 in Node.js downloads
|
||||
if [ "$NODE_ARCH" = "x64" ]; then
|
||||
NODE_FILENAME="x64"
|
||||
elif [ "$NODE_ARCH" = "arm64" ]; then
|
||||
NODE_FILENAME="arm64"
|
||||
fi
|
||||
|
||||
# Debug output
|
||||
echo "Node filename: $NODE_FILENAME"
|
||||
|
||||
PKG_DIR=dist/trilium-linux-${ARCH}-server
|
||||
echo "Package directory: $PKG_DIR"
|
||||
|
||||
if [ "$1" != "DONTCOPY" ]
|
||||
then
|
||||
./bin/copy-trilium.sh $PKG_DIR
|
||||
# Need to modify copy-trilium.sh to accept the target directory
|
||||
./bin/copy-trilium.sh "$PKG_DIR"
|
||||
fi
|
||||
|
||||
cd dist
|
||||
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz
|
||||
tar xfJ node-v${NODE_VERSION}-linux-x64.tar.xz
|
||||
rm node-v${NODE_VERSION}-linux-x64.tar.xz
|
||||
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||
tar xfJ node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||
rm node-v${NODE_VERSION}-linux-${NODE_FILENAME}.tar.xz
|
||||
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/include/node
|
||||
@@ -37,4 +73,4 @@ VERSION=`jq -r ".version" package.json`
|
||||
|
||||
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/katex/dist/",
|
||||
"node_modules/dayjs/",
|
||||
"node_modules/force-graph/dist/",
|
||||
"node_modules/boxicons/css/",
|
||||
"node_modules/boxicons/fonts/",
|
||||
"node_modules/mermaid/dist/",
|
||||
@@ -101,7 +100,8 @@ const copy = async () => {
|
||||
"node_modules/codemirror/keymap/",
|
||||
"node_modules/mind-elixir/dist/",
|
||||
"node_modules/@highlightjs/cdn-assets/languages",
|
||||
"node_modules/@highlightjs/cdn-assets/styles"
|
||||
"node_modules/@highlightjs/cdn-assets/styles",
|
||||
"node_modules/leaflet/dist"
|
||||
];
|
||||
|
||||
for (const folder of nodeModulesFolder) {
|
||||
|
||||
@@ -68,7 +68,6 @@ find $DIR -name "*.ts" -type f -delete
|
||||
|
||||
d="$DIR"/src/public
|
||||
[[ -d "$d"/app-dist ]] || mkdir -pv "$d"/app-dist
|
||||
cp "$d"/app/share.js "$d"/app-dist/
|
||||
cp -r "$d"/app/doc_notes "$d"/app-dist/
|
||||
|
||||
rm -rf "$d"/app
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* Usage: node src/tools/generate_document.js 1000
|
||||
* Usage: tsx ./generate_document.ts 1000
|
||||
* will create 1000 new notes and some clones into the current document.db
|
||||
*/
|
||||
|
||||
import sqlInit from "../services/sql_init.js";
|
||||
import noteService from "../services/notes.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import cls from "../services/cls.js";
|
||||
import cloningService from "../services/cloning.js";
|
||||
import sqlInit from "../src/services/sql_init.js";
|
||||
import noteService from "../src/services/notes.js";
|
||||
import attributeService from "../src/services/attributes.js";
|
||||
import cls from "../src/services/cls.js";
|
||||
import cloningService from "../src/services/cloning.js";
|
||||
import loremIpsum from "lorem-ipsum";
|
||||
import "../becca/entity_constructor.js";
|
||||
import "../src/becca/entity_constructor.js";
|
||||
|
||||
const noteCount = parseInt(process.argv[2]);
|
||||
|
||||
@@ -90,4 +90,6 @@ async function start() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(cls.wrap(start));
|
||||
// @TriliumNextTODO sqlInit.dbReady never seems to resolve so program hangs
|
||||
// see https://github.com/TriliumNext/Notes/issues/1020
|
||||
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => console.error(err));
|
||||
@@ -27,3 +27,8 @@ keyPath=
|
||||
# once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients.
|
||||
# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
|
||||
trustedReverseProxy=false
|
||||
|
||||
[Sync]
|
||||
#syncServerHost=
|
||||
#syncServerTimeout=
|
||||
#syncServerProxy=
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,5 @@
|
||||
import http from "http";
|
||||
import ini from "ini";
|
||||
import fs from "fs";
|
||||
import dataDir from "./src/services/data_dir.js";
|
||||
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
|
||||
import config from "./src/services/config.js";
|
||||
|
||||
if (config.Network.https) {
|
||||
// built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
|
||||
|
||||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -12,7 +12,7 @@ function getDataKey(password: any) {
|
||||
|
||||
const encryptedDataKey = getOption("encryptedDataKey");
|
||||
|
||||
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
|
||||
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey);
|
||||
|
||||
return decryptedDataKey;
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -16,7 +16,7 @@ function decryptString(dataKey: any, cipherText: any) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function decrypt(key: any, cipherText: any, ivLength = 13) {
|
||||
function decrypt(key: any, cipherText: any) {
|
||||
if (cipherText === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -27,6 +27,8 @@ function decrypt(key: any, cipherText: any, ivLength = 13) {
|
||||
|
||||
try {
|
||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
|
||||
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
|
||||
const iv = cipherTextBufferWithIv.slice(0, ivLength);
|
||||
|
||||
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
|
||||
|
||||
@@ -464,9 +464,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.7.2",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz",
|
||||
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==",
|
||||
"version": "11.8.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
|
||||
"integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
@@ -1516,9 +1516,9 @@
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"version": "11.7.2",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.7.2.tgz",
|
||||
"integrity": "sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==",
|
||||
"version": "11.8.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
|
||||
"integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { test, expect, Page, BrowserContext } from "@playwright/test";
|
||||
import App from "../support/app";
|
||||
|
||||
test("renders ELK flowchart", async ({ page, context }) => {
|
||||
await testAriaSnapshot({
|
||||
page, context,
|
||||
noteTitle: "Flowchart ELK on",
|
||||
snapshot: `
|
||||
- document:
|
||||
- paragraph: A
|
||||
- paragraph: B
|
||||
- paragraph: C
|
||||
- paragraph: Guarantee
|
||||
- paragraph: User attributes
|
||||
- paragraph: Master data
|
||||
- paragraph: Exchange Rate
|
||||
- paragraph: Profit Centers
|
||||
- paragraph: Vendor Partners
|
||||
- paragraph: Work Situation
|
||||
- paragraph: Customer
|
||||
- paragraph: Profit Centers
|
||||
- paragraph: Guarantee
|
||||
- text: Interfaces for B
|
||||
`
|
||||
})
|
||||
});
|
||||
|
||||
test("renders standard flowchart", async ({ page, context }) => {
|
||||
await testAriaSnapshot({
|
||||
page, context,
|
||||
noteTitle: "Flowchart ELK off",
|
||||
snapshot: `
|
||||
- document:
|
||||
- paragraph: Guarantee
|
||||
- paragraph: User attributes
|
||||
- paragraph: Master data
|
||||
- paragraph: Exchange Rate
|
||||
- paragraph: Profit Centers
|
||||
- paragraph: Vendor Partners
|
||||
- paragraph: Work Situation
|
||||
- paragraph: Customer
|
||||
- paragraph: Profit Centers
|
||||
- paragraph: Guarantee
|
||||
- paragraph: A
|
||||
- paragraph: B
|
||||
- paragraph: C
|
||||
- text: Interfaces for B
|
||||
`
|
||||
})
|
||||
});
|
||||
|
||||
interface AriaTestOpts {
|
||||
page: Page;
|
||||
context: BrowserContext;
|
||||
noteTitle: string;
|
||||
snapshot: string;
|
||||
}
|
||||
|
||||
async function testAriaSnapshot({ page, context, noteTitle, snapshot }: AriaTestOpts) {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab(noteTitle);
|
||||
|
||||
const svgData = app.currentNoteSplit.locator(".mermaid-render svg");
|
||||
await expect(svgData).toBeVisible();
|
||||
await expect(svgData).toMatchAriaSnapshot(snapshot);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
import App from "../support/app";
|
||||
|
||||
test("displays simple map", async ({ page, context }) => {
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -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,6 +2,7 @@ import { expect, Locator, Page } from "@playwright/test";
|
||||
import type { BrowserContext } from "@playwright/test";
|
||||
|
||||
interface GotoOpts {
|
||||
url?: string;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ export default class App {
|
||||
|
||||
readonly tabBar: Locator;
|
||||
readonly noteTree: Locator;
|
||||
readonly launcherBar: Locator;
|
||||
readonly currentNoteSplit: Locator;
|
||||
readonly sidebar: Locator;
|
||||
|
||||
@@ -22,25 +24,32 @@ export default class App {
|
||||
|
||||
this.tabBar = page.locator(".tab-row-widget-container");
|
||||
this.noteTree = page.locator(".tree-wrapper");
|
||||
this.launcherBar = page.locator("#launcher-container");
|
||||
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)")
|
||||
this.sidebar = page.locator("#right-pane");
|
||||
}
|
||||
|
||||
async goto(opts: GotoOpts = {}) {
|
||||
async goto({ url, isMobile }: GotoOpts = {}) {
|
||||
await this.context.addCookies([
|
||||
{
|
||||
url: BASE_URL,
|
||||
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.
|
||||
await expect(this.page.locator(".tree"))
|
||||
.toContainText("Trilium Integration Test");
|
||||
await this.closeAllTabs();
|
||||
if (url === "/") {
|
||||
await expect(this.page.locator(".tree"))
|
||||
.toContainText("Trilium Integration Test");
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
}
|
||||
|
||||
async goToNoteInNewTab(noteTitle: string) {
|
||||
|
||||
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -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,5 +0,0 @@
|
||||
For bug reports, **PLEASE mention version of Trilium you're using** and also include **log files** from following location:
|
||||
|
||||
* `/home/[user]/.local/share/trilium-data/log` for Linux
|
||||
* `C:\Users\[user]\AppData\Roaming\trilium-data\log` for Windows Vista and up
|
||||
* `/Users/[user]/Library/Application Support/trilium-data/log` for Mac OS
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "mermaid-elk",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mermaid-elk",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "mermaid-elk",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "cross-env node --import ../../loader-register.js ../../node_modules/webpack/bin/webpack.js -c webpack.config.cjs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
mode: "production",
|
||||
entry: "../../node_modules/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs",
|
||||
output: {
|
||||
library: "MERMAID_ELK",
|
||||
filename: "elk.min.js",
|
||||
path: path.resolve(__dirname),
|
||||
libraryTarget: "umd",
|
||||
libraryExport: "default"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1
|
||||
})
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Used to register the loader with Node.js
|
||||
// This is used to avoid the warning message when using the loader
|
||||
// Can be removed if this PR is merged:
|
||||
// https://github.com/TypeStrong/ts-node/pull/2073
|
||||
// Then probably can change webpack comand to
|
||||
// "webpack": "cross-env NODE_OPTIONS=--import=ts-node/esm webpack -c webpack.config.ts",
|
||||
|
||||
import { register } from "node:module";
|
||||
import { pathToFileURL } from "node:url";
|
||||
register("ts-node/esm", pathToFileURL("./"));
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "TriliumNext Notes",
|
||||
"description": "Build your personal knowledge base with TriliumNext Notes",
|
||||
"version": "0.91.3-beta",
|
||||
"version": "0.91.4-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "./dist/electron-main.js",
|
||||
"author": {
|
||||
@@ -28,18 +28,17 @@
|
||||
"start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .",
|
||||
"start-electron-nix": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
||||
"start-electron-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
|
||||
"start-electron-no-dir-nix": "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": "electron-rebuild --version 33.3.1 && npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
||||
"qstart-electron": "npm run switch-electron && npm run start-electron",
|
||||
"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-frontend-docs": "rimraf ./docs/frontend_api && jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/basic_widget.js src/public/app/widgets/note_context_aware_widget.js src/public/app/widgets/right_panel_widget.js",
|
||||
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
|
||||
"webpack": "cross-env node --import ./loader-register.js node_modules/webpack/bin/webpack.js -c webpack.config.ts",
|
||||
"webpack": "tsx node_modules/webpack/bin/webpack.js -c webpack.config.ts",
|
||||
"test-playwright": "playwright test",
|
||||
"test-jasmine": "cross-env TRILIUM_DATA_DIR=./data-test tsx ./node_modules/jasmine/bin/jasmine.js",
|
||||
"test-es6": "tsx -r esm spec-es6/attribute_parser.spec.ts",
|
||||
"test": "npm run test-jasmine && npm run test-es6",
|
||||
"test": "cross-env TRILIUM_DATA_DIR=./data-test vitest",
|
||||
"test-coverage": "cross-env TRILIUM_DATA_DIR=./data-test vitest --coverage",
|
||||
"start-electron-forge": "npm run prepare-dist && electron-forge start",
|
||||
"make-electron": "npm run webpack && npm run prepare-dist && electron-forge make",
|
||||
"package-electron": "electron-forge package",
|
||||
@@ -49,7 +48,7 @@
|
||||
"integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000",
|
||||
"generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
|
||||
"ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts",
|
||||
"prettier-check": "prettier . --check",
|
||||
"prettier-fix": "prettier . --write"
|
||||
@@ -60,13 +59,15 @@
|
||||
"@excalidraw/excalidraw": "0.17.6",
|
||||
"@highlightjs/cdn-assets": "11.11.1",
|
||||
"@mermaid-js/layout-elk": "0.1.7",
|
||||
"@mind-elixir/node-menu": "1.0.3",
|
||||
"@mind-elixir/node-menu": "1.0.4",
|
||||
"@triliumnext/express-partial-content": "1.0.1",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"axios": "1.7.9",
|
||||
"better-sqlite3": "11.7.2",
|
||||
"better-sqlite3": "11.8.1",
|
||||
"bootstrap": "5.3.3",
|
||||
"boxicons": "2.1.4",
|
||||
"cheerio": "1.0.0",
|
||||
@@ -89,16 +90,16 @@
|
||||
"express": "4.21.2",
|
||||
"express-rate-limit": "7.5.0",
|
||||
"express-session": "1.18.1",
|
||||
"force-graph": "1.47.2",
|
||||
"fs-extra": "11.2.0",
|
||||
"force-graph": "1.49.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"helmet": "8.0.0",
|
||||
"html": "1.0.0",
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "24.2.1",
|
||||
"i18next": "24.2.2",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"i18next-http-backend": "3.0.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"image-type": "5.2.0",
|
||||
"ini": "5.0.0",
|
||||
"is-animated": "2.0.2",
|
||||
@@ -110,8 +111,9 @@
|
||||
"jquery.fancytree": "2.38.4",
|
||||
"jsdom": "26.0.0",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.19",
|
||||
"katex": "0.16.21",
|
||||
"knockout": "3.5.1",
|
||||
"leaflet": "1.9.4",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "15.0.6",
|
||||
"mermaid": "11.4.1",
|
||||
@@ -146,14 +148,14 @@
|
||||
"yauzl": "3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "7.6.0",
|
||||
"@electron-forge/maker-deb": "7.6.0",
|
||||
"@electron-forge/maker-dmg": "7.6.0",
|
||||
"@electron-forge/maker-squirrel": "7.6.0",
|
||||
"@electron-forge/maker-zip": "7.6.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.6.0",
|
||||
"@electron-forge/cli": "7.6.1",
|
||||
"@electron-forge/maker-deb": "7.6.1",
|
||||
"@electron-forge/maker-dmg": "7.6.1",
|
||||
"@electron-forge/maker-squirrel": "7.6.1",
|
||||
"@electron-forge/maker-zip": "7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.6.1",
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
@@ -175,7 +177,8 @@
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
"@types/node": "22.10.6",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/safe-compare": "1.1.2",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/sax": "1.2.7",
|
||||
@@ -186,13 +189,13 @@
|
||||
"@types/stream-throttle": "0.1.4",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/ws": "8.5.13",
|
||||
"@types/ws": "8.5.14",
|
||||
"@types/xml2js": "0.4.14",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "33.3.1",
|
||||
"electron": "34.0.1",
|
||||
"esm": "3.2.25",
|
||||
"iconsur": "1.7.0",
|
||||
"jasmine": "5.5.0",
|
||||
"jsdoc": "4.0.4",
|
||||
"lorem-ipsum": "2.0.8",
|
||||
@@ -200,11 +203,11 @@
|
||||
"prettier": "3.4.2",
|
||||
"rcedit": "4.0.1",
|
||||
"rimraf": "6.0.1",
|
||||
"ts-node": "10.9.2",
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.19.2",
|
||||
"typedoc": "0.27.6",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.4",
|
||||
"webpack": "5.97.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-dev-middleware": "7.4.2"
|
||||
|
||||
@@ -41,10 +41,10 @@ export default defineConfig({
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { describe, it, execute, expect } from "./mini_test.ts";
|
||||
|
||||
import { getPlatformAppDataDir, getDataDirs} from "../src/services/data_dir.ts"
|
||||
|
||||
|
||||
|
||||
describe("data_dir.ts unit tests", () => {
|
||||
|
||||
describe("#getPlatformAppDataDir()", () => {
|
||||
|
||||
type TestCaseGetPlatformAppDataDir = [
|
||||
description: string,
|
||||
fnValue: Parameters<typeof getPlatformAppDataDir>,
|
||||
expectedValueFn: (val: ReturnType<typeof getPlatformAppDataDir>) => boolean
|
||||
]
|
||||
const testCases: TestCaseGetPlatformAppDataDir[] = [
|
||||
|
||||
[
|
||||
"w/ unsupported OS it should return 'null'",
|
||||
["aix", undefined],
|
||||
(val) => val === null
|
||||
],
|
||||
|
||||
[
|
||||
"w/ win32 and no APPDATA set it should return 'null'",
|
||||
["win32", undefined],
|
||||
(val) => val === null
|
||||
],
|
||||
|
||||
[
|
||||
"w/ win32 and set APPDATA it should return set 'APPDATA'",
|
||||
["win32", "AppData"],
|
||||
(val) => val === "AppData"
|
||||
],
|
||||
|
||||
[
|
||||
"w/ linux it should return '/.local/share'",
|
||||
["linux", undefined],
|
||||
(val) => val !== null && val.endsWith("/.local/share")
|
||||
],
|
||||
|
||||
[
|
||||
"w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share",
|
||||
["linux", "FakeAppData"],
|
||||
(val) => val !== null && val.endsWith("/.local/share")
|
||||
],
|
||||
|
||||
[
|
||||
"w/ darwin it should return /Library/Application Support",
|
||||
["darwin", undefined],
|
||||
(val) => val !== null && val.endsWith("/Library/Application Support")
|
||||
],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [testDescription, value, isExpected] = testCase;
|
||||
return it(testDescription, () => {
|
||||
const actual = getPlatformAppDataDir(...value);
|
||||
const result = isExpected(actual);
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
describe("#getTriliumDataDir", () => {
|
||||
// TODO
|
||||
})
|
||||
|
||||
describe("#getDataDirs()", () => {
|
||||
|
||||
const envKeys: Omit<keyof ReturnType<typeof getDataDirs>, "TRILIUM_DATA_DIR">[] = [
|
||||
"DOCUMENT_PATH",
|
||||
"BACKUP_DIR",
|
||||
"LOG_DIR",
|
||||
"ANONYMIZED_DB_DIR",
|
||||
"CONFIG_INI_PATH",
|
||||
];
|
||||
|
||||
const setMockedEnv = (prefix: string | null) => {
|
||||
envKeys.forEach(key => {
|
||||
if (prefix) {
|
||||
process.env[`TRILIUM_${key}`] = `${prefix}_${key}`
|
||||
} else {
|
||||
delete process.env[`TRILIUM_${key}`]
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
it("w/ process.env values present, it should return an object using values from process.env", () => {
|
||||
|
||||
// set mocked values
|
||||
const mockValuePrefix = "MOCK";
|
||||
setMockedEnv(mockValuePrefix);
|
||||
|
||||
// get result
|
||||
const result = getDataDirs(`${mockValuePrefix}_TRILIUM_DATA_DIR`);
|
||||
|
||||
for (const key in result) {
|
||||
expect(result[key]).toEqual(`${mockValuePrefix}_${key}`)
|
||||
}
|
||||
})
|
||||
|
||||
it("w/ NO process.env values present, it should return an object using supplied TRILIUM_DATA_DIR as base", () => {
|
||||
|
||||
// make sure values are undefined
|
||||
setMockedEnv(null);
|
||||
|
||||
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
|
||||
const result = getDataDirs(mockDataDir);
|
||||
|
||||
for (const key in result) {
|
||||
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it("should ignore attempts to change a property on the returned object", () => {
|
||||
|
||||
// make sure values are undefined
|
||||
setMockedEnv(null);
|
||||
|
||||
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"
|
||||
const result = getDataDirs(mockDataDir);
|
||||
|
||||
//@ts-expect-error - attempt to change value of readonly property
|
||||
result.BACKUP_DIR = "attempt to change";
|
||||
|
||||
for (const key in result) {
|
||||
expect(result[key].startsWith(mockDataDir)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
execute()
|
||||
@@ -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,3 +0,0 @@
|
||||
describe("Notes", () => {
|
||||
it("zzz", () => {});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
describe("Utils", () => {
|
||||
|
||||
@@ -1528,7 +1528,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -116,7 +116,8 @@ export const ALLOWED_NOTE_TYPES = [
|
||||
"book",
|
||||
"webView",
|
||||
"code",
|
||||
"mindMap"
|
||||
"mindMap",
|
||||
"geoMap"
|
||||
] as const;
|
||||
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ interface DateLimits {
|
||||
maxDate: string;
|
||||
}
|
||||
|
||||
interface SimilarNote {
|
||||
score: number;
|
||||
notePath: string[];
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
function filterUrlValue(value: string) {
|
||||
return value
|
||||
.replace(/https?:\/\//gi, "")
|
||||
@@ -247,7 +253,7 @@ function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) {
|
||||
return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId);
|
||||
}
|
||||
|
||||
async function findSimilarNotes(noteId: string) {
|
||||
async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefined> {
|
||||
const results = [];
|
||||
let i = 0;
|
||||
|
||||
@@ -417,6 +423,7 @@ async function findSimilarNotes(noteId: string) {
|
||||
|
||||
// this takes care of note hoisting
|
||||
if (!notePath) {
|
||||
// TODO: This return is suspicious, it should probably be continue
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import type LoadResults from "../services/load_results.js";
|
||||
import type { Attribute } from "../services/attribute_parser.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
|
||||
import type { ContextMenuEvent } from "../menus/context_menu.js";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
@@ -69,7 +70,8 @@ export interface ExecuteCommandData extends CommandData {
|
||||
*/
|
||||
export type CommandMappings = {
|
||||
"api-log-messages": CommandData;
|
||||
focusOnDetail: Required<CommandData>;
|
||||
focusTree: CommandData,
|
||||
focusOnDetail: CommandData;
|
||||
focusOnSearchDefinition: Required<CommandData>;
|
||||
searchNotes: CommandData & {
|
||||
searchString?: string;
|
||||
@@ -102,12 +104,15 @@ export type CommandMappings = {
|
||||
openNoteInNewTab: CommandData;
|
||||
openNoteInNewSplit: CommandData;
|
||||
openNoteInNewWindow: CommandData;
|
||||
hideLeftPane: CommandData;
|
||||
showLeftPane: CommandData;
|
||||
|
||||
openInTab: ContextMenuCommandData;
|
||||
openNoteInSplit: ContextMenuCommandData;
|
||||
toggleNoteHoisting: ContextMenuCommandData;
|
||||
insertNoteAfter: ContextMenuCommandData;
|
||||
insertChildNote: ContextMenuCommandData;
|
||||
delete: ContextMenuCommandData;
|
||||
protectSubtree: ContextMenuCommandData;
|
||||
unprotectSubtree: ContextMenuCommandData;
|
||||
openBulkActionsDialog: ContextMenuCommandData;
|
||||
@@ -192,6 +197,10 @@ export type CommandMappings = {
|
||||
setZoomFactorAndSave: {
|
||||
zoomFactor: string;
|
||||
}
|
||||
|
||||
// Geomap
|
||||
deleteFromMap: { noteId: string },
|
||||
openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent }
|
||||
};
|
||||
|
||||
type EventMappings = {
|
||||
@@ -226,9 +235,15 @@ type EventMappings = {
|
||||
activeContextChanged: {
|
||||
noteContext: NoteContext;
|
||||
};
|
||||
beforeNoteSwitch: {
|
||||
noteContext: NoteContext;
|
||||
};
|
||||
beforeNoteContextRemove: {
|
||||
ntxIds: string[];
|
||||
};
|
||||
noteSwitched: {
|
||||
noteContext: NoteContext;
|
||||
notePath: string;
|
||||
notePath: string | null;
|
||||
};
|
||||
noteSwitchedAndActivatedEvent: {
|
||||
noteContext: NoteContext;
|
||||
@@ -247,12 +262,16 @@ type EventMappings = {
|
||||
noteId: string;
|
||||
};
|
||||
hoistedNoteChanged: {
|
||||
ntxId: string;
|
||||
noteId: string;
|
||||
ntxId: string | null;
|
||||
};
|
||||
contextsReopenedEvent: {
|
||||
mainNtxId: string;
|
||||
tabPosition: number;
|
||||
};
|
||||
noteDetailRefreshed: {
|
||||
ntxId?: string | null;
|
||||
};
|
||||
noteContextReorderEvent: {
|
||||
oldMainNtxId: string;
|
||||
newMainNtxId: string;
|
||||
@@ -265,6 +284,15 @@ type EventMappings = {
|
||||
};
|
||||
exportSvg: {
|
||||
ntxId: string;
|
||||
};
|
||||
geoMapCreateChildNote: {
|
||||
ntxId: string | null | undefined; // TODO: deduplicate ntxId
|
||||
};
|
||||
tabReorder: {
|
||||
ntxIdsInOrder: string[]
|
||||
};
|
||||
refreshNoteList: {
|
||||
noteId: string;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
return this;
|
||||
}
|
||||
|
||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
|
||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
|
||||
try {
|
||||
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
|
||||
|
||||
@@ -61,7 +61,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
||||
}
|
||||
}
|
||||
|
||||
triggerEvent(name: string, data = {}): Promise<unknown> | undefined | null {
|
||||
triggerEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown> | undefined | null {
|
||||
return this.parent?.triggerEvent(name, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
|
||||
notePath?: string | null;
|
||||
noteId?: string | null;
|
||||
private parentNoteId?: string | null;
|
||||
parentNoteId?: string | null;
|
||||
viewScope?: ViewScope;
|
||||
|
||||
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import electronContextMenu from "./menus/electron_context_menu.js";
|
||||
import glob from "./services/glob.js";
|
||||
import { t } from "./services/i18n.js";
|
||||
import options from "./services/options.js";
|
||||
import type ElectronRemote from "@electron/remote";
|
||||
import type Electron from "electron";
|
||||
|
||||
await appContext.earlyInit();
|
||||
|
||||
@@ -44,10 +46,9 @@ if (utils.isElectron()) {
|
||||
}
|
||||
|
||||
function initOnElectron() {
|
||||
const electron = utils.dynamicRequire("electron");
|
||||
const electron: typeof Electron = utils.dynamicRequire("electron");
|
||||
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
|
||||
|
||||
const electronRemote = utils.dynamicRequire("@electron/remote");
|
||||
const electronRemote: typeof ElectronRemote = utils.dynamicRequire("@electron/remote");
|
||||
const currentWindow = electronRemote.getCurrentWindow();
|
||||
const style = window.getComputedStyle(document.body);
|
||||
|
||||
@@ -58,7 +59,7 @@ function initOnElectron() {
|
||||
}
|
||||
}
|
||||
|
||||
function initTitleBarButtons(style, currentWindow) {
|
||||
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||
if (window.glob.platform === "win32") {
|
||||
const applyWindowsOverlay = () => {
|
||||
const color = style.getPropertyValue("--native-titlebar-background");
|
||||
@@ -81,9 +82,14 @@ function initTitleBarButtons(style, currentWindow) {
|
||||
}
|
||||
}
|
||||
|
||||
function initTransparencyEffects(style, currentWindow) {
|
||||
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||
if (window.glob.platform === "win32") {
|
||||
const material = style.getPropertyValue("--background-material");
|
||||
currentWindow.setBackgroundMaterial(material);
|
||||
// TriliumNextTODO: find a nicer way to make TypeScript happy – unfortunately TS did not like Array.includes here
|
||||
const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
|
||||
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
|
||||
if (foundBgMaterialOption) {
|
||||
currentWindow.setBackgroundMaterial(foundBgMaterialOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default class FBlob {
|
||||
/**
|
||||
* @throws Error in case of invalid JSON
|
||||
*/
|
||||
getJsonContent(): unknown {
|
||||
getJsonContent<T>(): T | null {
|
||||
if (!this.content || !this.content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ const NOTE_TYPE_ICONS = {
|
||||
launcher: "bx bx-link",
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap"
|
||||
mindMap: "bx bx-sitemap",
|
||||
geoMap: "bx bx-map-alt"
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,12 +36,12 @@ const NOTE_TYPE_ICONS = {
|
||||
* end user. Those types should be used only for checking against, they are
|
||||
* not for direct use.
|
||||
*/
|
||||
type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap";
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap";
|
||||
|
||||
interface NotePathRecord {
|
||||
export interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
isInHoistedSubTree: boolean;
|
||||
isSearch: boolean;
|
||||
isSearch?: boolean;
|
||||
notePath: string[];
|
||||
isHidden: boolean;
|
||||
}
|
||||
@@ -401,14 +402,14 @@ class FNote {
|
||||
return notePaths;
|
||||
}
|
||||
|
||||
getSortedNotePathRecords(hoistedNoteId = "root") {
|
||||
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
|
||||
const isHoistedRoot = hoistedNoteId === "root";
|
||||
|
||||
const notePaths = this.getAllNotePaths().map((path) => ({
|
||||
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
||||
notePath: path,
|
||||
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
|
||||
isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
|
||||
isSearch: path.find((noteId) => froca.notes[noteId].type === "search"),
|
||||
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
|
||||
isHidden: path.includes("_hidden")
|
||||
}));
|
||||
|
||||
@@ -515,10 +516,10 @@ class FNote {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [name] - label name to filter
|
||||
* @param name - label name to filter
|
||||
* @returns all note's labels (attributes with type label), including inherited ones
|
||||
*/
|
||||
getOwnedLabels(name: string) {
|
||||
getOwnedLabels(name?: string) {
|
||||
return this.getOwnedAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
import options from "../services/options.js";
|
||||
import utils from "../services/utils.js";
|
||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -200,6 +201,7 @@ export default class DesktopLayout {
|
||||
.child(new ShowHighlightsListWidgetButton())
|
||||
.child(new CodeButtonsWidget())
|
||||
.child(new RelationMapButtons())
|
||||
.child(new GeoMapButtons())
|
||||
.child(new CopyImageReferenceButton())
|
||||
.child(new SvgExportButton())
|
||||
.child(new BacklinksWidget())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CommandNames } from "../components/app_context.js";
|
||||
import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
interface ContextMenuOptions<T extends CommandNames> {
|
||||
@@ -8,6 +9,8 @@ interface ContextMenuOptions<T extends CommandNames> {
|
||||
orientation?: "left";
|
||||
selectMenuItemHandler: MenuHandler<T>;
|
||||
items: MenuItem<T>[];
|
||||
/** On mobile, if set to `true` then the context menu is shown near the element. If `false` (default), then the context menu is shown at the bottom of the screen. */
|
||||
forcePositionOnMobile?: boolean;
|
||||
}
|
||||
|
||||
interface MenuSeparatorItem {
|
||||
@@ -29,6 +32,7 @@ export interface MenuCommandItem<T extends CommandNames> {
|
||||
|
||||
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||
|
||||
class ContextMenu {
|
||||
private $widget: JQuery<HTMLElement>;
|
||||
@@ -54,12 +58,15 @@ class ContextMenu {
|
||||
async show<T extends CommandNames>(options: ContextMenuOptions<T>) {
|
||||
this.options = options;
|
||||
|
||||
note_tooltip.dismissAllTooltips();
|
||||
|
||||
if (this.$widget.hasClass("show")) {
|
||||
// The menu is already visible. Hide the menu then open it again
|
||||
// at the new location to re-trigger the opening animation.
|
||||
await this.hide();
|
||||
}
|
||||
|
||||
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
|
||||
this.$cover.addClass("show");
|
||||
$("body").addClass("context-menu-shown");
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
|
||||
|
||||
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
|
||||
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
|
||||
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers";
|
||||
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers";
|
||||
const isVisibleItem = (parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers");
|
||||
const isAvailableItem = (parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers");
|
||||
const isItem = isVisibleItem || isAvailableItem;
|
||||
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
|
||||
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import contextMenu from "./context_menu.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
||||
import appContext, { type CommandNames } from "../components/app_context.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: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
||||
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (!hoistedNoteId) {
|
||||
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
|
||||
}
|
||||
|
||||
if (command === "openNoteInNewTab") {
|
||||
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewSplit") {
|
||||
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
|
||||
const { ntxId } = subContexts[subContexts.length - 1];
|
||||
|
||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewWindow") {
|
||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||
}
|
||||
}
|
||||
items: getItems(),
|
||||
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
|
||||
});
|
||||
}
|
||||
|
||||
function getItems(): MenuItem<CommandNames>[] {
|
||||
return [
|
||||
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
|
||||
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
|
||||
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }
|
||||
];
|
||||
}
|
||||
|
||||
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
|
||||
if (!hoistedNoteId) {
|
||||
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
|
||||
}
|
||||
|
||||
if (command === "openNoteInNewTab") {
|
||||
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewSplit") {
|
||||
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
|
||||
const { ntxId } = subContexts[subContexts.length - 1];
|
||||
|
||||
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
|
||||
} else if (command === "openNoteInNewWindow") {
|
||||
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getItems,
|
||||
handleLinkContextMenuItem,
|
||||
openContextMenu
|
||||
};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { EntityRowNames } from "./services/load_results.js";
|
||||
|
||||
interface Entity {
|
||||
isDeleted?: boolean;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with src/services/entity_changes_interface.ts
|
||||
export interface EntityChange {
|
||||
id?: number | null;
|
||||
noteId?: string;
|
||||
entityName: EntityRowNames;
|
||||
entityId: string;
|
||||
entity?: any;
|
||||
entity?: Entity;
|
||||
positions?: Record<string, number>;
|
||||
hash: string;
|
||||
utcDateChanged?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as attributeParser from "../src/public/app/services/attribute_parser.js";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import attributeParser from "./attribute_parser.js";
|
||||
|
||||
import { describe, it, expect, execute } from "./mini_test.js";
|
||||
|
||||
describe("Lexing", () => {
|
||||
it("simple label", () => {
|
||||
@@ -40,8 +40,10 @@ describe("Lexing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parser", () => {
|
||||
describe.todo("Parser", () => {
|
||||
/* #TODO
|
||||
it("simple label", () => {
|
||||
|
||||
const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));
|
||||
|
||||
expect(attrs.length).toEqual(1);
|
||||
@@ -85,6 +87,7 @@ describe("Parser", () => {
|
||||
expect(attrs[0].name).toEqual("token");
|
||||
expect(attrs[0].value).toEqual("NFi2gL4xtPxM");
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
describe("error cases", () => {
|
||||
@@ -96,5 +99,3 @@ describe("error cases", () => {
|
||||
expect(() => attributeParser.lexAndParse("#")).toThrow(`Attribute name is empty, please fill the name.`);
|
||||
});
|
||||
});
|
||||
|
||||
execute();
|
||||
@@ -290,14 +290,16 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
attachment.update(ec.entity as FAttachmentRow);
|
||||
} else {
|
||||
const attachmentRow = ec.entity as FAttachmentRow;
|
||||
const note = froca.notes[attachmentRow.ownerId];
|
||||
if (ec.entity) {
|
||||
if (attachment) {
|
||||
attachment.update(ec.entity as FAttachmentRow);
|
||||
} else {
|
||||
const attachmentRow = ec.entity as FAttachmentRow;
|
||||
const note = froca.notes[attachmentRow.ownerId];
|
||||
|
||||
if (note?.attachments) {
|
||||
note.attachments.push(new FAttachment(froca, attachmentRow));
|
||||
if (note?.attachments) {
|
||||
note.attachments.push(new FAttachment(froca, attachmentRow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,26 +68,10 @@ const WHEEL_ZOOM: Library = {
|
||||
js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
|
||||
};
|
||||
|
||||
const FORCE_GRAPH: Library = {
|
||||
js: ["node_modules/force-graph/dist/force-graph.min.js"]
|
||||
};
|
||||
|
||||
const MERMAID: Library = {
|
||||
js: ["node_modules/mermaid/dist/mermaid.min.js"]
|
||||
};
|
||||
|
||||
/**
|
||||
* The ELK extension of Mermaid.js, which supports more advanced layouts.
|
||||
* See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
|
||||
*/
|
||||
const MERMAID_ELK: Library = {
|
||||
js: ["libraries/mermaid-elk/elk.min.js"]
|
||||
};
|
||||
|
||||
const EXCALIDRAW: Library = {
|
||||
js: ["node_modules/react/umd/react.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/@excalidraw/excalidraw/dist/excalidraw.production.min.js"]
|
||||
};
|
||||
|
||||
const MARKJS: Library = {
|
||||
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
|
||||
};
|
||||
@@ -122,6 +106,10 @@ const HIGHLIGHT_JS: Library = {
|
||||
}
|
||||
};
|
||||
|
||||
const LEAFLET: Library = {
|
||||
css: [ "node_modules/leaflet/dist/leaflet.css" ],
|
||||
}
|
||||
|
||||
async function requireLibrary(library: Library) {
|
||||
if (library.css) {
|
||||
library.css.map((cssUrl) => requireCss(cssUrl));
|
||||
@@ -209,11 +197,9 @@ export default {
|
||||
CALENDAR_WIDGET,
|
||||
KATEX,
|
||||
WHEEL_ZOOM,
|
||||
FORCE_GRAPH,
|
||||
MERMAID,
|
||||
MERMAID_ELK,
|
||||
EXCALIDRAW,
|
||||
MARKJS,
|
||||
I18NEXT,
|
||||
HIGHLIGHT_JS
|
||||
HIGHLIGHT_JS,
|
||||
LEAFLET
|
||||
};
|
||||
|
||||
@@ -234,7 +234,7 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
|
||||
return goToLinkExt(evt, hrefLink, $link);
|
||||
}
|
||||
|
||||
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement>) {
|
||||
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
|
||||
if (hrefLink?.startsWith("data:")) {
|
||||
return true;
|
||||
}
|
||||
@@ -242,20 +242,17 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (hrefLink?.startsWith("#fn")) {
|
||||
if (hrefLink?.startsWith("#fn") && $link) {
|
||||
return handleFootnote(hrefLink, $link);
|
||||
}
|
||||
|
||||
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
|
||||
|
||||
const ctrlKey = utils.isCtrlKey(evt);
|
||||
const isLeftClick = evt.which === 1;
|
||||
const isMiddleClick = evt.which === 2;
|
||||
const isLeftClick = ("which" in evt && evt.which === 1);
|
||||
const isMiddleClick = ("which" in evt && evt.which === 2);
|
||||
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
|
||||
|
||||
const leftClick = evt.which === 1;
|
||||
const middleClick = evt.which === 2;
|
||||
|
||||
if (notePath) {
|
||||
if (openInNewTab) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
|
||||
@@ -276,7 +273,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent, hrefLink: string | und
|
||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||
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/")) {
|
||||
window.open(hrefLink, "_blank");
|
||||
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
|
||||
|
||||
@@ -8,6 +8,7 @@ interface NoteRow {
|
||||
}
|
||||
|
||||
interface BranchRow {
|
||||
noteId?: string;
|
||||
branchId: string;
|
||||
componentId: string;
|
||||
parentNoteId?: string;
|
||||
@@ -157,7 +158,7 @@ export default class LoadResults {
|
||||
return Object.keys(this.noteIdToComponentId);
|
||||
}
|
||||
|
||||
isNoteReloaded(noteId: string, componentId = null) {
|
||||
isNoteReloaded(noteId: string | undefined, componentId: string | null = null) {
|
||||
if (!noteId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import library_loader from "./library_loader.js";
|
||||
|
||||
let elkLoaded = false;
|
||||
|
||||
/**
|
||||
@@ -22,7 +20,6 @@ export async function loadElkIfNeeded(mermaidContent: string) {
|
||||
});
|
||||
if (parsedContent?.config?.layout === "elk") {
|
||||
elkLoaded = true;
|
||||
await library_loader.requireLibrary(library_loader.MERMAID_ELK);
|
||||
mermaid.registerLayoutLoaders(MERMAID_ELK);
|
||||
mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: deduplicate with /src/services/import/mime_type_definitions.ts
|
||||
|
||||
/**
|
||||
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
|
||||
*/
|
||||
|
||||
@@ -18,11 +18,11 @@ function setupGlobalTooltip() {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanUpTooltips();
|
||||
dismissAllTooltips();
|
||||
});
|
||||
}
|
||||
|
||||
function cleanUpTooltips() {
|
||||
function dismissAllTooltips() {
|
||||
$(".note-tooltip").remove();
|
||||
}
|
||||
|
||||
@@ -102,12 +102,12 @@ async function mouseEnterHandler(this: HTMLElement) {
|
||||
customClass: linkId
|
||||
});
|
||||
|
||||
cleanUpTooltips();
|
||||
dismissAllTooltips();
|
||||
$(this).tooltip("show");
|
||||
|
||||
// Dismiss the tooltip immediately if a link was clicked inside the tooltip.
|
||||
$(`.${tooltipClass} a`).on("click", (e) => {
|
||||
cleanUpTooltips();
|
||||
dismissAllTooltips();
|
||||
});
|
||||
|
||||
// the purpose of the code below is to:
|
||||
@@ -117,7 +117,7 @@ async function mouseEnterHandler(this: HTMLElement) {
|
||||
const checkTooltip = () => {
|
||||
if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) {
|
||||
// cursor is neither over the link nor over the tooltip, user likely is not interested
|
||||
cleanUpTooltips();
|
||||
dismissAllTooltips();
|
||||
} else {
|
||||
setTimeout(checkTooltip, 1000);
|
||||
}
|
||||
@@ -172,5 +172,6 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) {
|
||||
|
||||
export default {
|
||||
setupGlobalTooltip,
|
||||
setupElementTooltip
|
||||
setupElementTooltip,
|
||||
dismissAllTooltips
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ async function getNoteTypeItems(command?: NoteTypeCommandNames) {
|
||||
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
|
||||
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
|
||||
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
|
||||
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }
|
||||
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
|
||||
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
|
||||
];
|
||||
|
||||
const templateNoteIds = await server.get<string[]>("search-templates");
|
||||
|
||||
@@ -36,10 +36,14 @@ class Options {
|
||||
|
||||
getInt(key: string) {
|
||||
const value = this.arr?.[key];
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
return parseInt(value);
|
||||
if (typeof value == "string") {
|
||||
return parseInt(value);
|
||||
}
|
||||
console.warn("Attempting to read int for unsupported value: ", value);
|
||||
return null;
|
||||
}
|
||||
|
||||
getFloat(key: string) {
|
||||
|
||||
@@ -43,6 +43,14 @@ export default class SpacedUpdate {
|
||||
return allSaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normally {@link scheduleUpdate()} would actually trigger the update only once per {@link updateInterval}. If the method is called 200 times within 20s, it will execute only 20 times.
|
||||
* Sometimes, if the updates are continuous this would cause a performance impact. Resetting the time ensures that the calls to {@link triggerUpdate} have stopped before actually triggering an update.
|
||||
*/
|
||||
resetUpdateTimer() {
|
||||
this.lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
triggerUpdate() {
|
||||
if (!this.changed) {
|
||||
return;
|
||||
|
||||
@@ -97,7 +97,7 @@ function isMac() {
|
||||
return navigator.platform.indexOf("Mac") > -1;
|
||||
}
|
||||
|
||||
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
|
||||
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) {
|
||||
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
||||
}
|
||||
|
||||
@@ -308,7 +308,9 @@ function dynamicRequire(moduleName: string) {
|
||||
if (typeof __non_webpack_require__ !== "undefined") {
|
||||
return __non_webpack_require__(moduleName);
|
||||
} else {
|
||||
return require(moduleName);
|
||||
// explicitly pass as string and not as expression to suppress webpack warning
|
||||
// 'Critical dependency: the request of a dependency is an expression'
|
||||
return require(`${moduleName}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @param noteId of the given note to be fetched. If false, fetches current note.
|
||||
*/
|
||||
async function fetchNote(noteId = null) {
|
||||
async function fetchNote(noteId: string | null = null) {
|
||||
if (!noteId) {
|
||||
noteId = document.body.getAttribute("data-note-id");
|
||||
}
|
||||
@@ -25,3 +25,9 @@ document.addEventListener(
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// workaround to prevent webpack from removing "fetchNote" as dead code:
|
||||
// add fetchNote as property to the window object
|
||||
Object.defineProperty(window, "fetchNote", {
|
||||
value: fetchNote
|
||||
});
|
||||
@@ -43,6 +43,7 @@ interface CustomGlobals {
|
||||
appCssNoteIds: string[];
|
||||
triliumVersion: string;
|
||||
TRILIUM_SAFE_MODE: boolean;
|
||||
platform?: typeof process.platform;
|
||||
}
|
||||
|
||||
type RequireMethod = (moduleName: string) => any;
|
||||
@@ -155,13 +156,12 @@ declare global {
|
||||
registerLayoutLoaders(loader: MermaidLoader);
|
||||
parse(content: string, opts: {
|
||||
suppressErrors: true
|
||||
}): {
|
||||
}): Promise<{
|
||||
config: {
|
||||
layout: string;
|
||||
}
|
||||
}
|
||||
}>
|
||||
};
|
||||
var MERMAID_ELK: MermaidLoader;
|
||||
|
||||
var CKEditor: {
|
||||
BalloonEditor: {
|
||||
|
||||
@@ -193,7 +193,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
* Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
|
||||
* @returns whether the widget is enabled.
|
||||
*/
|
||||
isEnabled() {
|
||||
isEnabled(): boolean | null | undefined {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
*/
|
||||
doRender() {}
|
||||
|
||||
toggleInt(show: boolean) {
|
||||
toggleInt(show: boolean | null | undefined) {
|
||||
this.$widget.toggleClass("hidden-int", !show);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ const TPL = `
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 5px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
body.mobile .global-menu .dropdown-submenu .dropdown-menu {
|
||||
@@ -162,8 +162,7 @@ const TPL = `
|
||||
|
||||
<li class="dropdown-item dropdown-submenu">
|
||||
<span class="dropdown-toggle">
|
||||
<span class="bx bx-chip"></span>
|
||||
${t("global_menu.advanced")}
|
||||
<span class="bx bx-chip"></span>${t("global_menu.advanced")}
|
||||
</span>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
|
||||
@@ -2,9 +2,11 @@ import options from "../../services/options.js";
|
||||
import splitService from "../../services/resizer.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
export default class LeftPaneToggleWidget extends CommandButtonWidget {
|
||||
constructor(isHorizontalLayout) {
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
|
||||
this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
|
||||
@@ -32,7 +34,7 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
|
||||
splitService.setupLeftPaneResizer(options.is("leftPaneVisible"));
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }) {
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isOptionReloaded("leftPaneVisible")) {
|
||||
this.refreshIcon();
|
||||
}
|
||||
@@ -42,7 +42,7 @@ const TPL = `
|
||||
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
|
||||
<span class="bx bx-paperclip"></span> ${t("note_actions.convert_into_attachment")}
|
||||
</li>
|
||||
|
||||
|
||||
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
|
||||
<span class="bx bx-extension"></span> ${t("note_actions.re_render_note")}<kbd data-command="renderActiveNote"></kbd>
|
||||
</li>
|
||||
@@ -54,15 +54,15 @@ const TPL = `
|
||||
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
|
||||
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd></li>
|
||||
|
||||
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
|
||||
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t("note_actions.import_files")}</li>
|
||||
|
||||
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t("note_actions.export_note")}</li>
|
||||
|
||||
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ const TPL = `
|
||||
<span class="bx bx-code"></span> ${t("note_actions.note_source")}<kbd data-command="showNoteSource"></kbd>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
@@ -89,10 +89,10 @@ const TPL = `
|
||||
|
||||
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t("note_actions.delete_note")}</li>
|
||||
|
||||
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
|
||||
|
||||
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
|
||||
<span class="bx bx-paperclip"></span> ${t("note_actions.note_attachments")}<kbd data-command="showAttachments"></kbd>
|
||||
</li>
|
||||
@@ -154,7 +154,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type));
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$printActiveNoteButton, ["text", "code"].includes(note.type));
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import options from "../../services/options.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import appContext, { type EventData } from "../../components/app_context.js";
|
||||
import type Component from "../../components/component.js";
|
||||
|
||||
export default class LeftPaneContainer extends FlexContainer {
|
||||
export default class LeftPaneContainer extends FlexContainer<Component> {
|
||||
constructor() {
|
||||
super("column");
|
||||
|
||||
@@ -15,13 +16,13 @@ export default class LeftPaneContainer extends FlexContainer {
|
||||
return super.isEnabled() && options.is("leftPaneVisible");
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }) {
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isOptionReloaded("leftPaneVisible")) {
|
||||
const visible = this.isEnabled();
|
||||
this.toggleInt(visible);
|
||||
|
||||
if (visible) {
|
||||
this.triggerEvent("focusTree");
|
||||
this.triggerEvent("focusTree", {});
|
||||
} else {
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId });
|
||||
@@ -1,6 +1,10 @@
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import keyboardActionsService from "../../services/keyboard_actions.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import type CommandButtonWidget from "../buttons/command_button.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { NoteType } from "../../entities/fnote.js";
|
||||
import type { EventData, EventNames } from "../../components/app_context.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="ribbon-container">
|
||||
@@ -8,11 +12,11 @@ const TPL = `
|
||||
.ribbon-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-top-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -21,10 +25,10 @@ const TPL = `
|
||||
flex-grow: 1;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-title {
|
||||
color: var(--muted-text-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
min-width: 24px;
|
||||
flex-basis: 24px;
|
||||
max-width: max-content;
|
||||
@@ -36,7 +40,7 @@ const TPL = `
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-title.active {
|
||||
color: var(--main-text-color);
|
||||
border-bottom: 3px solid var(--main-text-color);
|
||||
@@ -44,7 +48,7 @@ const TPL = `
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-title:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -52,11 +56,11 @@ const TPL = `
|
||||
.ribbon-tab-title:hover {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-title:first-of-type {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-spacer {
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
@@ -64,41 +68,41 @@ const TPL = `
|
||||
flex-grow: 1;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-spacer:last-of-type {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
max-width: 10000px;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-button-container {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-button-container > * {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-body {
|
||||
display: none;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-left: 10px;
|
||||
margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
|
||||
}
|
||||
|
||||
|
||||
.ribbon-body.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-title-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.ribbon-tab-title.active .ribbon-tab-title-label {
|
||||
display: inline;
|
||||
}
|
||||
@@ -108,11 +112,21 @@ const TPL = `
|
||||
<div class="ribbon-tab-container"></div>
|
||||
<div class="ribbon-button-container"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="ribbon-body-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
|
||||
private lastActiveComponentId?: string | null;
|
||||
private lastNoteType?: NoteType;
|
||||
|
||||
private ribbonWidgets: NoteContextAwareWidget[];
|
||||
private buttonWidgets: CommandButtonWidget[];
|
||||
private $tabContainer!: JQuery<HTMLElement>;
|
||||
private $buttonContainer!: JQuery<HTMLElement>;
|
||||
private $bodyContainer!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -122,10 +136,10 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteContext.viewScope.viewMode === "default";
|
||||
return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
|
||||
}
|
||||
|
||||
ribbon(widget) {
|
||||
ribbon(widget: NoteContextAwareWidget) { // TODO: Base class
|
||||
super.child(widget);
|
||||
|
||||
this.ribbonWidgets.push(widget);
|
||||
@@ -133,7 +147,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
return this;
|
||||
}
|
||||
|
||||
button(widget) {
|
||||
button(widget: CommandButtonWidget) {
|
||||
super.child(widget);
|
||||
|
||||
this.buttonWidgets.push(widget);
|
||||
@@ -163,7 +177,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
toggleRibbonTab($ribbonTitle, refreshActiveTab = true) {
|
||||
toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
|
||||
const activate = !$ribbonTitle.hasClass("active");
|
||||
|
||||
this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
|
||||
@@ -181,14 +195,15 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
|
||||
const activeChild = this.getActiveRibbonWidget();
|
||||
|
||||
if (activeChild && (refreshActiveTab || !wasAlreadyActive)) {
|
||||
if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
|
||||
const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
|
||||
|
||||
if (refreshActiveTab) {
|
||||
if (handleEventPromise) {
|
||||
handleEventPromise.then(() => activeChild.focus?.());
|
||||
handleEventPromise.then(() => (activeChild as any).focus()); // TODO: Base class
|
||||
} else {
|
||||
activeChild.focus?.();
|
||||
// TODO: Base class
|
||||
(activeChild as any)?.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +218,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
await super.noteSwitched();
|
||||
}
|
||||
|
||||
async refreshWithNote(note, noExplicitActivation = false) {
|
||||
async refreshWithNote(note: FNote, noExplicitActivation = false) {
|
||||
this.lastNoteType = note.type;
|
||||
|
||||
let $ribbonTabToActivate, $lastActiveRibbon;
|
||||
@@ -211,7 +226,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
this.$tabContainer.empty();
|
||||
|
||||
for (const ribbonWidget of this.ribbonWidgets) {
|
||||
const ret = await ribbonWidget.getTitle(note);
|
||||
// TODO: Base class for ribbon widget
|
||||
const ret = await (ribbonWidget as any).getTitle(note);
|
||||
|
||||
if (!ret.show) {
|
||||
continue;
|
||||
@@ -219,8 +235,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
|
||||
const $ribbonTitle = $('<div class="ribbon-tab-title">')
|
||||
.attr("data-ribbon-component-id", ribbonWidget.componentId)
|
||||
.attr("data-ribbon-component-name", ribbonWidget.name)
|
||||
.append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", ribbonWidget.toggleCommand))
|
||||
.attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
|
||||
.append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", (ribbonWidget as any).toggleCommand)) // TODO: base class
|
||||
.append(" ")
|
||||
.append($('<span class="ribbon-tab-title-label">').text(ret.title));
|
||||
|
||||
@@ -238,7 +254,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
|
||||
keyboardActionsService.getActions().then((actions) => {
|
||||
this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
|
||||
title: function () {
|
||||
title: () => {
|
||||
const toggleCommandName = $(this).attr("data-toggle-command");
|
||||
const action = actions.find((act) => act.actionName === toggleCommandName);
|
||||
const title = $(this).attr("data-title");
|
||||
@@ -246,7 +262,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
if (action && action.effectiveShortcuts.length > 0) {
|
||||
return `${title} (${action.effectiveShortcuts.join(", ")})`;
|
||||
} else {
|
||||
return title;
|
||||
return title ?? "";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -263,27 +279,27 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
isRibbonTabActive(name) {
|
||||
isRibbonTabActive(name: string) {
|
||||
const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
|
||||
|
||||
return $ribbonComponent.hasClass("active");
|
||||
}
|
||||
|
||||
ensureOwnedAttributesAreOpen(ntxId) {
|
||||
if (this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
|
||||
ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
|
||||
if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
|
||||
this.toggleRibbonTabWithName("ownedAttributes", ntxId);
|
||||
}
|
||||
}
|
||||
|
||||
addNewLabelEvent({ ntxId }) {
|
||||
addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
|
||||
this.ensureOwnedAttributesAreOpen(ntxId);
|
||||
}
|
||||
|
||||
addNewRelationEvent({ ntxId }) {
|
||||
addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
|
||||
this.ensureOwnedAttributesAreOpen(ntxId);
|
||||
}
|
||||
|
||||
toggleRibbonTabWithName(name, ntxId) {
|
||||
toggleRibbonTabWithName(name: string, ntxId?: string) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return false;
|
||||
}
|
||||
@@ -295,23 +311,23 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(name, data) {
|
||||
handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
|
||||
const PREFIX = "toggleRibbonTab";
|
||||
|
||||
if (name.startsWith(PREFIX)) {
|
||||
let componentName = name.substr(PREFIX.length);
|
||||
componentName = componentName[0].toLowerCase() + componentName.substr(1);
|
||||
|
||||
this.toggleRibbonTabWithName(componentName, data.ntxId);
|
||||
this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
|
||||
} else {
|
||||
return super.handleEvent(name, data);
|
||||
}
|
||||
}
|
||||
|
||||
async handleEventInChildren(name, data) {
|
||||
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
|
||||
if (["activeContextChanged", "setNoteContext"].includes(name)) {
|
||||
// won't trigger .refresh();
|
||||
await super.handleEventInChildren("setNoteContext", data);
|
||||
await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
|
||||
} else if (this.isEnabled() || name === "initialRenderComplete") {
|
||||
const activeRibbonWidget = this.getActiveRibbonWidget();
|
||||
|
||||
@@ -326,8 +342,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }) {
|
||||
if (loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
|
||||
// note type influences the list of available ribbon tabs the most
|
||||
// check for the type is so that we don't update on each title rename
|
||||
this.lastNoteType = this.note.type;
|
||||
@@ -338,7 +358,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
noteTypeMimeChangedEvent() {
|
||||
async noteTypeMimeChangedEvent() {
|
||||
// We are ignoring the event which triggers a refresh since it is usually already done by a different
|
||||
// event and causing a race condition in which the items appear twice.
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import attributeService from "../services/attributes.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
|
||||
type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled";
|
||||
|
||||
const TPL = `
|
||||
<div class="dropdown editability-select-widget">
|
||||
@@ -8,7 +12,11 @@ const TPL = `
|
||||
.editability-dropdown {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
|
||||
.editability-dropdown .dropdown-item {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.editability-dropdown .dropdown-item div {
|
||||
font-size: small;
|
||||
color: var(--muted-text-color);
|
||||
@@ -23,7 +31,7 @@ const TPL = `
|
||||
<a class="dropdown-item" href="#" data-editability="auto">
|
||||
<span class="check">✓</span>
|
||||
${t("editability_select.auto")}
|
||||
<div>${t("editability_select.note_is_editable")}</div>
|
||||
<div>${t("editability_select.note_is_editable")}</div>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" data-editability="readOnly">
|
||||
<span class="check">✓</span>
|
||||
@@ -40,9 +48,15 @@ const TPL = `
|
||||
`;
|
||||
|
||||
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
|
||||
|
||||
private dropdown!: bootstrap.Dropdown;
|
||||
private $editabilityActiveDesc!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
// TODO: Remove once bootstrap is added to webpack.
|
||||
//@ts-ignore
|
||||
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
|
||||
|
||||
this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
|
||||
@@ -52,24 +66,28 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
|
||||
|
||||
const editability = $(e.target).closest("[data-editability]").attr("data-editability");
|
||||
|
||||
if (!this.note || !this.noteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ownedAttr of this.note.getOwnedLabels()) {
|
||||
if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
|
||||
await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
|
||||
}
|
||||
}
|
||||
|
||||
if (editability !== "auto") {
|
||||
if (editability && editability !== "auto") {
|
||||
await attributeService.addLabel(this.noteId, editability);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
let editability = "auto";
|
||||
async refreshWithNote(note: FNote) {
|
||||
let editability: Editability = "auto";
|
||||
|
||||
if (this.note.isLabelTruthy("readOnly")) {
|
||||
if (this.note?.isLabelTruthy("readOnly")) {
|
||||
editability = "readOnly";
|
||||
} else if (this.note.isLabelTruthy("autoReadOnlyDisabled")) {
|
||||
} else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
|
||||
editability = "autoReadOnlyDisabled";
|
||||
}
|
||||
|
||||
@@ -85,7 +103,7 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
|
||||
this.$editabilityActiveDesc.text(labels[editability]);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }) {
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js"
|
||||
|
||||
const TPL = `\
|
||||
<div class="geo-map-buttons">
|
||||
<style>
|
||||
.geo-map-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.leaflet-pane {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.geo-map-buttons {
|
||||
contain: none;
|
||||
background: var(--main-background-color);
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button type="button"
|
||||
class="geo-map-create-child-note floating-button btn bx bx-folder-plus"
|
||||
title="${t("geo-map.create-child-note-title")}" />
|
||||
</div>`;
|
||||
|
||||
export default class GeoMapButtons extends NoteContextAwareWidget {
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.note?.type === "geoMap";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.find(".geo-map-create-child-note").on("click", () => this.triggerEvent("geoMapCreateChildNote", { ntxId: this.ntxId }));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,16 +13,16 @@ const TPL = `
|
||||
<button type="button"
|
||||
class="relation-map-create-child-note floating-button btn bx bx-folder-plus"
|
||||
title="${t("relation_map_buttons.create_child_note_title")}"></button>
|
||||
|
||||
|
||||
<button type="button"
|
||||
class="relation-map-reset-pan-zoom floating-button btn bx bx-crop"
|
||||
title="${t("relation_map_buttons.reset_pan_zoom_title")}"></button>
|
||||
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button"
|
||||
class="relation-map-zoom-in floating-button btn bx bx-zoom-in"
|
||||
title="${t("relation_map_buttons.zoom_in_title")}"></button>
|
||||
|
||||
|
||||
<button type="button"
|
||||
class="relation-map-zoom-out floating-button btn bx bx-zoom-out"
|
||||
title="${t("relation_map_buttons.zoom_out_title")}"></button>
|
||||
@@ -43,6 +43,7 @@ export default class RelationMapButtons extends NoteContextAwareWidget {
|
||||
this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out");
|
||||
this.$resetPanZoomButton = this.$widget.find(".relation-map-reset-pan-zoom");
|
||||
|
||||
// TODO: Deduplicate object creation here.
|
||||
this.$createChildNote.on("click", () => this.triggerEvent("relationMapCreateChildNote", { ntxId: this.ntxId }));
|
||||
this.$resetPanZoomButton.on("click", () => this.triggerEvent("relationMapResetPanZoom", { ntxId: this.ntxId }));
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import server from "../../services/server.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="backlinks-widget">
|
||||
@@ -14,7 +15,7 @@ const TPL = `
|
||||
.backlinks-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.backlinks-ticker {
|
||||
border-radius: 10px;
|
||||
border-color: var(--main-border-color);
|
||||
@@ -25,11 +26,11 @@ const TPL = `
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.backlinks-count {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.backlinks-items {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
@@ -42,29 +43,41 @@ const TPL = `
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
.backlink-excerpt {
|
||||
border-left: 2px solid var(--main-border-color);
|
||||
padding-left: 10px;
|
||||
opacity: 80%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
|
||||
.backlink-excerpt .backlink-link { /* the actual backlink */
|
||||
font-weight: bold;
|
||||
background-color: yellow;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div class="backlinks-ticker">
|
||||
<span class="backlinks-count"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="backlinks-items" style="display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// TODO: Deduplicate with server
|
||||
interface Backlink {
|
||||
noteId: string;
|
||||
relationName?: string;
|
||||
excerpts?: string[];
|
||||
}
|
||||
|
||||
export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $count!: JQuery<HTMLElement>;
|
||||
private $items!: JQuery<HTMLElement>;
|
||||
private $ticker!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$count = this.$widget.find(".backlinks-count");
|
||||
@@ -73,7 +86,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$count.on("click", () => {
|
||||
this.$items.toggle();
|
||||
this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
|
||||
this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10);
|
||||
|
||||
if (this.$items.is(":visible")) {
|
||||
this.renderBacklinks();
|
||||
@@ -83,7 +96,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.clearItems();
|
||||
|
||||
if (this.noteContext?.viewScope?.viewMode !== "default") {
|
||||
@@ -92,7 +105,8 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
// can't use froca since that would count only relations from loaded notes
|
||||
const resp = await server.get(`note-map/${this.noteId}/backlink-count`);
|
||||
// TODO: Deduplicate response type
|
||||
const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`);
|
||||
|
||||
if (!resp || !resp.count) {
|
||||
this.toggle(false);
|
||||
@@ -106,7 +120,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
);
|
||||
}
|
||||
|
||||
toggle(show) {
|
||||
toggle(show: boolean) {
|
||||
this.$widget.toggleClass("hidden-no-content", !show);
|
||||
}
|
||||
|
||||
@@ -121,7 +135,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$items.empty();
|
||||
|
||||
const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
|
||||
const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`);
|
||||
|
||||
if (!backlinks.length) {
|
||||
return;
|
||||
@@ -143,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
if (backlink.relationName) {
|
||||
$item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
|
||||
} else {
|
||||
$item.append(...backlink.excerpts);
|
||||
$item.append(...backlink.excerpts ?? []);
|
||||
}
|
||||
|
||||
this.$items.append($item);
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Map } from "leaflet";
|
||||
import library_loader from "../services/library_loader.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = `\
|
||||
<div class="geo-map-widget">
|
||||
<style>
|
||||
.note-detail-geo-map,
|
||||
.geo-map-widget,
|
||||
.geo-map-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="geo-map-container"></div>
|
||||
</div>`
|
||||
|
||||
export type Leaflet = typeof import("leaflet");
|
||||
export type InitCallback = ((L: Leaflet) => void);
|
||||
|
||||
export default class GeoMapWidget extends NoteContextAwareWidget {
|
||||
|
||||
map?: Map;
|
||||
$container!: JQuery<HTMLElement>;
|
||||
private initCallback?: InitCallback;
|
||||
|
||||
constructor(widgetMode: "type", initCallback?: InitCallback) {
|
||||
super();
|
||||
this.initCallback = initCallback;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$container = this.$widget.find(".geo-map-container");
|
||||
|
||||
library_loader.requireLibrary(library_loader.LEAFLET)
|
||||
.then(async () => {
|
||||
const L = (await import("leaflet")).default;
|
||||
|
||||
const map = L.map(this.$container[0], {
|
||||
worldCopyJump: true
|
||||
});
|
||||
|
||||
this.map = map;
|
||||
if (this.initCallback) {
|
||||
this.initCallback(L);
|
||||
}
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ interface Category {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Icon {
|
||||
export interface Icon {
|
||||
name: string;
|
||||
slug: string;
|
||||
category_id: number;
|
||||
|
||||
@@ -4,6 +4,8 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { loadElkIfNeeded } from "../services/mermaid.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
|
||||
const TPL = `<div class="mermaid-widget">
|
||||
<style>
|
||||
@@ -39,8 +41,14 @@ const TPL = `<div class="mermaid-widget">
|
||||
let idCounter = 1;
|
||||
|
||||
export default class MermaidWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $display!: JQuery<HTMLElement>;
|
||||
private $errorContainer!: JQuery<HTMLElement>;
|
||||
private $errorMessage!: JQuery<HTMLElement>;
|
||||
private dirtyAttachment?: boolean;
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope.viewMode === "default";
|
||||
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
@@ -51,14 +59,14 @@ export default class MermaidWidget extends NoteContextAwareWidget {
|
||||
this.$errorMessage = this.$errorContainer.find(".error-content");
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$errorContainer.hide();
|
||||
|
||||
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
|
||||
|
||||
mermaid.mermaidAPI.initialize({
|
||||
startOnLoad: false,
|
||||
...getMermaidConfig()
|
||||
...getMermaidConfig() as any
|
||||
});
|
||||
|
||||
this.$display.empty();
|
||||
@@ -91,12 +99,11 @@ export default class MermaidWidget extends NoteContextAwareWidget {
|
||||
this.$display.attr("id", `mermaid-render-${idCounter}`);
|
||||
|
||||
WZoom.create(`#mermaid-render-${idCounter}`, {
|
||||
type: "html",
|
||||
maxScale: 50,
|
||||
speed: 1.3,
|
||||
zoomOnClick: false
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
console.warn(e);
|
||||
this.$errorMessage.text(e.message);
|
||||
this.$errorContainer.show();
|
||||
@@ -106,24 +113,28 @@ export default class MermaidWidget extends NoteContextAwareWidget {
|
||||
async renderSvg() {
|
||||
idCounter++;
|
||||
|
||||
if (!this.note) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const blob = await this.note.getBlob();
|
||||
const content = blob.content || "";
|
||||
const content = blob?.content || "";
|
||||
|
||||
await loadElkIfNeeded(content);
|
||||
const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
|
||||
return svg;
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }) {
|
||||
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
this.dirtyAttachment = true;
|
||||
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async exportSvgEvent({ ntxId }) {
|
||||
if (!this.isNoteContext(ntxId) || this.note.type !== "mermaid") {
|
||||
async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
|
||||
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,6 +150,7 @@ export function getMermaidConfig() {
|
||||
return {
|
||||
theme: mermaidTheme.trim(),
|
||||
securityLevel: "antiscript",
|
||||
// TODO: Are all these options correct?
|
||||
flow: { useMaxWidth: false },
|
||||
sequence: { useMaxWidth: false },
|
||||
gantt: { useMaxWidth: false },
|
||||
@@ -9,7 +9,9 @@ import { t } from "../../services/i18n.js";
|
||||
const TPL = `<button type="button" class="action-button bx" style="padding-top: 10px;"></button>`;
|
||||
|
||||
class MobileDetailMenuWidget extends BasicWidget {
|
||||
constructor(isHorizontalLayout) {
|
||||
private isHorizontalLayout: boolean;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
this.isHorizontalLayout = isHorizontalLayout;
|
||||
}
|
||||
@@ -31,9 +33,13 @@ class MobileDetailMenuWidget extends BasicWidget {
|
||||
],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
noteCreateService.createNote(appContext.tabManager.getActiveContextNotePath());
|
||||
noteCreateService.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
throw new Error("Cannot get note path to delete.");
|
||||
}
|
||||
|
||||
const branchId = await treeService.getBranchIdFromUrl(notePath);
|
||||
|
||||
if (!branchId) {
|
||||
@@ -46,7 +52,8 @@ class MobileDetailMenuWidget extends BasicWidget {
|
||||
} else {
|
||||
throw new Error(t("mobile_detail_menu.error_unrecognized_command", { command }));
|
||||
}
|
||||
}
|
||||
},
|
||||
forcePositionOnMobile: true
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import type NoteContext from "../components/note_context.js";
|
||||
class NoteContextAwareWidget extends BasicWidget {
|
||||
protected noteContext?: NoteContext;
|
||||
|
||||
isNoteContext(ntxId: string | null | undefined) {
|
||||
isNoteContext(ntxId: string | string[] | null | undefined) {
|
||||
if (Array.isArray(ntxId)) {
|
||||
return this.noteContext && ntxId.includes(this.noteContext.ntxId);
|
||||
return this.noteContext && this.noteContext.ntxId && ntxId.includes(this.noteContext.ntxId);
|
||||
} else {
|
||||
return this.noteContext && this.noteContext.ntxId === ntxId;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class NoteContextAwareWidget extends BasicWidget {
|
||||
*
|
||||
* @returns true when an active note exists
|
||||
*/
|
||||
isEnabled() {
|
||||
isEnabled(): boolean | null | undefined {
|
||||
return !!this.note;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
|
||||
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
|
||||
import MindMapWidget from "./type_widgets/mind_map.js";
|
||||
import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js";
|
||||
import GeoMapTypeWidget from "./type_widgets/geo_map.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail">
|
||||
@@ -39,7 +40,7 @@ const TPL = `
|
||||
font-family: var(--detail-font-family);
|
||||
font-size: var(--detail-font-size);
|
||||
}
|
||||
|
||||
|
||||
.note-detail.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -67,7 +68,8 @@ const typeWidgetClasses = {
|
||||
contentWidget: ContentWidgetTypeWidget,
|
||||
attachmentDetail: AttachmentDetailTypeWidget,
|
||||
attachmentList: AttachmentListTypeWidget,
|
||||
mindMap: MindMapWidget
|
||||
mindMap: MindMapWidget,
|
||||
geoMap: GeoMapTypeWidget
|
||||
};
|
||||
|
||||
export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
@@ -147,7 +149,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
this.$widget.toggleClass(
|
||||
"full-height",
|
||||
(!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") ||
|
||||
(!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") ||
|
||||
this.noteContext.viewScope.viewMode === "attachments"
|
||||
);
|
||||
}
|
||||
@@ -276,7 +278,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
<script src="${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
|
||||
<script>
|
||||
document.body.className += ' ck-content printed-content';
|
||||
|
||||
|
||||
renderMathInElement(document.body, {trust: true});
|
||||
</script>
|
||||
`,
|
||||
|
||||
@@ -2,6 +2,9 @@ import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import server from "../services/server.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import type { Icon } from "./icon_list.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-icon-widget dropdown">
|
||||
@@ -13,7 +16,7 @@ const TPL = `
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget button.note-icon {
|
||||
font-size: 180%;
|
||||
background-color: transparent;
|
||||
@@ -22,38 +25,38 @@ const TPL = `
|
||||
padding: 6px;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget button.note-icon:hover {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget .dropdown-menu {
|
||||
border-radius: 10px;
|
||||
border-width: 2px;
|
||||
box-shadow: 10px 10px 93px -25px black;
|
||||
padding: 10px 15px 10px 15px !important;
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget .filter-row {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 20px;
|
||||
display: flex;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget .filter-row span {
|
||||
display: block;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget .icon-list {
|
||||
height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget .icon-list span {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
@@ -61,25 +64,36 @@ const TPL = `
|
||||
border: 1px solid transparent;
|
||||
font-size: 180%;
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget .icon-list span:hover {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<button class="btn dropdown-toggle note-icon" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="${t("note_icon.change_note_icon")}"></button>
|
||||
<div class="dropdown-menu" aria-labelledby="note-path-list-button" style="width: 610px;">
|
||||
<div class="filter-row">
|
||||
<span>${t("note_icon.category")}</span> <select name="icon-category" class="form-control"></select>
|
||||
|
||||
|
||||
<span>${t("note_icon.search")}</span> <input type="text" name="icon-search" class="form-control" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="icon-list"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
interface IconToCountCache {
|
||||
iconClassToCountMap: Record<string, number>;
|
||||
}
|
||||
|
||||
export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $icon!: JQuery<HTMLElement>;
|
||||
private $iconList!: JQuery<HTMLElement>;
|
||||
private $iconCategory!: JQuery<HTMLElement>;
|
||||
private $iconSearch!: JQuery<HTMLElement>;
|
||||
private iconToCountCache!: Promise<IconToCountCache | null> | null;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$icon = this.$widget.find("button.note-icon");
|
||||
@@ -87,7 +101,9 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
this.$iconList.on("click", "span", async (e) => {
|
||||
const clazz = $(e.target).attr("class");
|
||||
|
||||
await attributeService.setLabel(this.noteId, this.note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass", clazz);
|
||||
if (this.noteId && this.note) {
|
||||
await attributeService.setLabel(this.noteId, this.note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass", clazz);
|
||||
}
|
||||
});
|
||||
|
||||
this.$iconCategory = this.$widget.find("select[name='icon-category']");
|
||||
@@ -97,7 +113,6 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
this.$iconSearch = this.$widget.find("input[name='icon-search']");
|
||||
this.$iconSearch.on("input", () => this.renderDropdown());
|
||||
|
||||
this.$notePathList = this.$widget.find(".note-path-list");
|
||||
this.$widget.on("show.bs.dropdown", async () => {
|
||||
const { categories } = (await import("./icon_list.js")).default;
|
||||
|
||||
@@ -113,18 +128,18 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$icon.removeClass().addClass(`${note.getIcon()} note-icon`);
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }) {
|
||||
if (loadResults.isNoteReloaded(this.noteId)) {
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const attr of loadResults.getAttributeRows()) {
|
||||
if (attr.type === "label" && ["iconClass", "workspaceIconClass"].includes(attr.name) && attributeService.isAffecting(attr, this.note)) {
|
||||
if (attr.type === "label" && ["iconClass", "workspaceIconClass"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note)) {
|
||||
this.refresh();
|
||||
break;
|
||||
}
|
||||
@@ -141,14 +156,18 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
this.$iconList.append(
|
||||
$(`<div style="text-align: center">`).append(
|
||||
$(`<button class="btn btn-sm">${t("note_icon.reset-default")}</button>`).on("click", () =>
|
||||
this.getIconLabels().forEach((label) => attributeService.removeAttributeById(this.noteId, label.attributeId))
|
||||
this.getIconLabels().forEach((label) => {
|
||||
if (this.noteId) {
|
||||
attributeService.removeAttributeById(this.noteId, label.attributeId);
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const categoryId = parseInt(this.$iconCategory.find("option:selected").val());
|
||||
const search = this.$iconSearch.val().trim().toLowerCase();
|
||||
const categoryId = parseInt(String(this.$iconCategory.find("option:selected")?.val()));
|
||||
const search = String(this.$iconSearch.val())?.trim()?.toLowerCase();
|
||||
|
||||
const filteredIcons = icons.filter((icon) => {
|
||||
if (categoryId && icon.category_id !== categoryId) {
|
||||
@@ -164,12 +183,14 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
return true;
|
||||
});
|
||||
|
||||
filteredIcons.sort((a, b) => {
|
||||
const countA = iconToCount[a.className] || 0;
|
||||
const countB = iconToCount[b.className] || 0;
|
||||
if (iconToCount) {
|
||||
filteredIcons.sort((a, b) => {
|
||||
const countA = iconToCount[a.className ?? ""] || 0;
|
||||
const countB = iconToCount[b.className ?? ""] || 0;
|
||||
|
||||
return countB - countA;
|
||||
});
|
||||
return countB - countA;
|
||||
});
|
||||
}
|
||||
|
||||
for (const icon of filteredIcons) {
|
||||
this.$iconList.append(this.renderIcon(icon));
|
||||
@@ -180,20 +201,23 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||
|
||||
async getIconToCountMap() {
|
||||
if (!this.iconToCountCache) {
|
||||
this.iconToCountCache = server.get("other/icon-usage");
|
||||
this.iconToCountCache = server.get<typeof this.iconToCountCache>("other/icon-usage");
|
||||
setTimeout(() => (this.iconToCountCache = null), 20000); // invalidate cache after 20 seconds
|
||||
}
|
||||
|
||||
return (await this.iconToCountCache).iconClassToCountMap;
|
||||
return (await this.iconToCountCache)?.iconClassToCountMap;
|
||||
}
|
||||
|
||||
renderIcon(icon) {
|
||||
renderIcon(icon: Icon) {
|
||||
return $("<span>")
|
||||
.addClass("bx " + icon.className)
|
||||
.attr("title", icon.name);
|
||||
}
|
||||
|
||||
getIconLabels() {
|
||||
if (!this.note) {
|
||||
return [];
|
||||
}
|
||||
return this.note.getOwnedLabels().filter((label) => ["workspaceIconClass", "iconClass"].includes(label.name));
|
||||
}
|
||||
}
|
||||