mirror of
https://github.com/zadam/trilium.git
synced 2025-11-10 16:39:02 +01:00
Merge remote-tracking branch 'origin/main' into react/type_widgets
This commit is contained in:
commit
5e83e6fa34
2
.github/actions/build-server/action.yml
vendored
2
.github/actions/build-server/action.yml
vendored
@ -12,7 +12,7 @@ runs:
|
|||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
79
.github/workflows/deploy-docs.yml
vendored
79
.github/workflows/deploy-docs.yml
vendored
@ -1,6 +1,4 @@
|
|||||||
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
|
name: Deploy Documentation
|
||||||
# This workflow builds and deploys your MkDocs site when changes are pushed to main
|
|
||||||
name: Deploy MkDocs Documentation
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Trigger on push to main branch
|
# Trigger on push to main branch
|
||||||
@ -11,11 +9,9 @@ on:
|
|||||||
# Only run when docs files change
|
# Only run when docs files change
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'README.md' # README is synced to docs/index.md
|
- 'apps/edit-docs/**'
|
||||||
- 'mkdocs.yml'
|
- 'apps/build-docs/**'
|
||||||
- 'requirements-docs.txt'
|
- 'packages/share-theme/**'
|
||||||
- '.github/workflows/deploy-docs.yml'
|
|
||||||
- 'scripts/fix-mkdocs-structure.ts'
|
|
||||||
|
|
||||||
# Allow manual triggering from Actions tab
|
# Allow manual triggering from Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@ -27,15 +23,13 @@ on:
|
|||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'README.md' # README is synced to docs/index.md
|
- 'apps/edit-docs/**'
|
||||||
- 'mkdocs.yml'
|
- 'apps/build-docs/**'
|
||||||
- 'requirements-docs.txt'
|
- 'packages/share-theme/**'
|
||||||
- '.github/workflows/deploy-docs.yml'
|
|
||||||
- 'scripts/fix-mkdocs-structure.ts'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
name: Build and Deploy MkDocs
|
name: Build and Deploy Documentation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
@ -49,72 +43,27 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.14'
|
|
||||||
cache: 'pip'
|
|
||||||
cache-dependency-path: 'requirements-docs.txt'
|
|
||||||
|
|
||||||
- name: Install MkDocs and Dependencies
|
|
||||||
run: |
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r requirements-docs.txt
|
|
||||||
env:
|
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
|
||||||
|
|
||||||
# Setup pnpm before fixing docs structure
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
# Setup Node.js with pnpm
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '24'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
# Install Node.js dependencies for the TypeScript script
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: pnpm install --frozen-lockfile
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Fix Documentation Structure
|
- name: Trigger build of documentation
|
||||||
run: |
|
run: pnpm docs:build
|
||||||
# Fix duplicate navigation entries by moving overview pages to index.md
|
|
||||||
pnpm run chore:fix-mkdocs-structure
|
|
||||||
|
|
||||||
- name: Build MkDocs Site
|
|
||||||
run: |
|
|
||||||
# Build with strict mode but allow expected warnings
|
|
||||||
mkdocs build --verbose || {
|
|
||||||
EXIT_CODE=$?
|
|
||||||
# Check if the only issue is expected warnings
|
|
||||||
if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
|
|
||||||
[ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
|
|
||||||
echo "✅ Build succeeded with expected warnings"
|
|
||||||
mkdocs build --verbose
|
|
||||||
else
|
|
||||||
echo "❌ Build failed with unexpected errors"
|
|
||||||
exit $EXIT_CODE
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Fix HTML Links
|
|
||||||
run: |
|
|
||||||
# Remove .md extensions from links in generated HTML
|
|
||||||
pnpm tsx ./scripts/fix-html-links.ts site
|
|
||||||
|
|
||||||
- name: Validate Built Site
|
- name: Validate Built Site
|
||||||
run: |
|
run: |
|
||||||
# Basic validation that important files exist
|
|
||||||
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
|
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
|
||||||
test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
|
test -f site/developer-guide/index.html || (echo "ERROR: site/developer-guide/index.html not found" && exit 1)
|
||||||
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
|
echo "✓ User Guide and Developer Guide built successfully"
|
||||||
echo "✅ Site validation passed"
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: ./.github/actions/deploy-to-cloudflare-pages
|
uses: ./.github/actions/deploy-to-cloudflare-pages
|
||||||
|
|||||||
2
.github/workflows/dev.yml
vendored
2
.github/workflows/dev.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/main-docker.yml
vendored
8
.github/workflows/main-docker.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install npm dependencies
|
- name: Install npm dependencies
|
||||||
@ -116,10 +116,10 @@ jobs:
|
|||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
image: ubuntu-24.04-arm
|
image: ubuntu-24.04-arm
|
||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile.legacy
|
||||||
platform: linux/arm/v7
|
platform: linux/arm/v7
|
||||||
image: ubuntu-24.04-arm
|
image: ubuntu-24.04-arm
|
||||||
- dockerfile: Dockerfile
|
- dockerfile: Dockerfile.legacy
|
||||||
platform: linux/arm/v8
|
platform: linux/arm/v8
|
||||||
image: ubuntu-24.04-arm
|
image: ubuntu-24.04-arm
|
||||||
runs-on: ${{ matrix.image }}
|
runs-on: ${{ matrix.image }}
|
||||||
@ -146,7 +146,7 @@ jobs:
|
|||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
|||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -50,7 +50,7 @@ jobs:
|
|||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
2
.github/workflows/website.yml
vendored
2
.github/workflows/website.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Set up node & dependencies
|
- name: Set up node & dependencies
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@ -37,20 +37,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
"@stylistic/eslint-plugin": "5.5.0",
|
"@stylistic/eslint-plugin": "5.5.0",
|
||||||
"@types/express": "5.0.4",
|
"@types/express": "5.0.5",
|
||||||
"@types/node": "22.18.12",
|
"@types/node": "24.10.0",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.34",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.38.0",
|
"eslint": "9.39.1",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"jsdoc": "4.0.5",
|
"jsdoc": "4.0.5",
|
||||||
"lorem-ipsum": "2.0.8",
|
"lorem-ipsum": "2.0.8",
|
||||||
"rcedit": "4.0.1",
|
"rcedit": "4.0.1",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.1.0",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1"
|
||||||
"typedoc": "0.28.14",
|
|
||||||
"typedoc-plugin-missing-exports": "4.1.2"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "0.6.6"
|
"appdmg": "0.6.6"
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"entryPoints": [
|
|
||||||
"src/services/backend_script_entrypoint.ts",
|
|
||||||
"src/public/app/services/frontend_script_entrypoint.ts"
|
|
||||||
],
|
|
||||||
"plugin": [
|
|
||||||
"typedoc-plugin-missing-exports"
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "html",
|
|
||||||
"path": "./docs/Script API"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
22
apps/build-docs/package.json
Normal file
22
apps/build-docs/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "build-docs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx ."
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Elian Doran <contact@eliandoran.me>",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"packageManager": "pnpm@10.20.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@redocly/cli": "2.11.0",
|
||||||
|
"archiver": "7.0.1",
|
||||||
|
"fs-extra": "11.3.2",
|
||||||
|
"react": "19.2.0",
|
||||||
|
"react-dom": "19.2.0",
|
||||||
|
"typedoc": "0.28.14",
|
||||||
|
"typedoc-plugin-missing-exports": "4.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
36
apps/build-docs/src/backend_script_entrypoint.ts
Normal file
36
apps/build-docs/src/backend_script_entrypoint.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* The backend script API is accessible to code notes with the "JS (backend)" language.
|
||||||
|
*
|
||||||
|
* The entire API is exposed as a single global: {@link api}
|
||||||
|
*
|
||||||
|
* @module Backend Script API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
||||||
|
* script note on the server side.
|
||||||
|
*
|
||||||
|
* Make sure to keep in line with backend's `script_context.ts`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
|
||||||
|
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
|
||||||
|
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
|
||||||
|
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
|
||||||
|
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
|
||||||
|
export type { BNote };
|
||||||
|
export type { default as BOption } from "../../server/src/becca/entities/boption.js";
|
||||||
|
export type { default as BRecentNote } from "../../server/src/becca/entities/brecent_note.js";
|
||||||
|
export type { default as BRevision } from "../../server/src/becca/entities/brevision.js";
|
||||||
|
|
||||||
|
import BNote from "../../server/src/becca/entities/bnote.js";
|
||||||
|
import BackendScriptApi, { type Api } from "../../server/src/services/backend_script_api.js";
|
||||||
|
|
||||||
|
export type { Api };
|
||||||
|
|
||||||
|
const fakeNote = new BNote();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
|
||||||
|
*/
|
||||||
|
export const api: Api = new BackendScriptApi(fakeNote, {});
|
||||||
147
apps/build-docs/src/build-docs.ts
Normal file
147
apps/build-docs/src/build-docs.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
|
||||||
|
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
|
||||||
|
import cls from "@triliumnext/server/src/services/cls.js";
|
||||||
|
import { dirname, join, resolve } from "path";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import * as fsExtra from "fs-extra";
|
||||||
|
import archiver from "archiver";
|
||||||
|
import { WriteStream } from "fs";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import BuildContext from "./context.js";
|
||||||
|
|
||||||
|
const DOCS_ROOT = "../../../docs";
|
||||||
|
const OUTPUT_DIR = "../../site";
|
||||||
|
|
||||||
|
async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
|
||||||
|
const note = await importData(sourcePath);
|
||||||
|
|
||||||
|
// Use a meaningful name for the temporary zip file
|
||||||
|
const zipName = outputSubDir || "user-guide";
|
||||||
|
const zipFilePath = `output-${zipName}.zip`;
|
||||||
|
try {
|
||||||
|
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
|
||||||
|
const branch = note.getParentBranches()[0];
|
||||||
|
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
|
||||||
|
"no-progress-reporting",
|
||||||
|
"export",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
|
||||||
|
await exportToZip(taskContext, branch, "share", fileOutputStream);
|
||||||
|
await waitForStreamToFinish(fileOutputStream);
|
||||||
|
|
||||||
|
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
|
||||||
|
const outputPath = outputSubDir ? join(OUTPUT_DIR, outputSubDir) : OUTPUT_DIR;
|
||||||
|
await extractZip(zipFilePath, outputPath);
|
||||||
|
} finally {
|
||||||
|
if (await fsExtra.exists(zipFilePath)) {
|
||||||
|
await fsExtra.rm(zipFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildDocsInner() {
|
||||||
|
const i18n = await import("@triliumnext/server/src/services/i18n.js");
|
||||||
|
await i18n.initializeTranslations();
|
||||||
|
|
||||||
|
const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
|
||||||
|
await sqlInit.createInitialDatabase(true);
|
||||||
|
|
||||||
|
// Wait for becca to be loaded before importing data
|
||||||
|
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
|
||||||
|
await beccaLoader.beccaLoaded;
|
||||||
|
|
||||||
|
// Build User Guide
|
||||||
|
console.log("Building User Guide...");
|
||||||
|
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
|
||||||
|
|
||||||
|
// Build Developer Guide
|
||||||
|
console.log("Building Developer Guide...");
|
||||||
|
await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide");
|
||||||
|
|
||||||
|
// Copy favicon.
|
||||||
|
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
|
||||||
|
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico"));
|
||||||
|
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
|
||||||
|
|
||||||
|
console.log("Documentation built successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importData(path: string) {
|
||||||
|
const buffer = await createImportZip(path);
|
||||||
|
const importService = (await import("../../server/src/services/import/zip.js")).default;
|
||||||
|
const TaskContext = (await import("../../server/src/services/task_context.js")).default;
|
||||||
|
const context = new TaskContext("no-progress-reporting", "importNotes", null);
|
||||||
|
const becca = (await import("../../server/src/becca/becca.js")).default;
|
||||||
|
|
||||||
|
const rootNote = becca.getRoot();
|
||||||
|
if (!rootNote) {
|
||||||
|
throw new Error("Missing root note for import.");
|
||||||
|
}
|
||||||
|
return await importService.importZip(context, buffer, rootNote, {
|
||||||
|
preserveIds: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createImportZip(path: string) {
|
||||||
|
const inputFile = "input.zip";
|
||||||
|
const archive = archiver("zip", {
|
||||||
|
zlib: { level: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Archive path is ", resolve(path))
|
||||||
|
archive.directory(path, "/");
|
||||||
|
|
||||||
|
const outputStream = fsExtra.createWriteStream(inputFile);
|
||||||
|
archive.pipe(outputStream);
|
||||||
|
archive.finalize();
|
||||||
|
await waitForStreamToFinish(outputStream);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fsExtra.readFile(inputFile);
|
||||||
|
} finally {
|
||||||
|
await fsExtra.rm(inputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForStreamToFinish(stream: WriteStream) {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
stream.on("finish", () => res());
|
||||||
|
stream.on("error", (err) => rej(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
|
||||||
|
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
|
||||||
|
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
|
||||||
|
// We ignore directories since they can appear out of order anyway.
|
||||||
|
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
|
||||||
|
const destPath = join(outputPath, entry.fileName);
|
||||||
|
const fileContent = await readContent(zip, entry);
|
||||||
|
|
||||||
|
await fsExtra.mkdirs(dirname(destPath));
|
||||||
|
await fs.writeFile(destPath, fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.readEntry();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function buildDocs({ gitRootDir }: BuildContext) {
|
||||||
|
// Build the share theme.
|
||||||
|
execSync(`pnpm run --filter share-theme build`, {
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: gitRootDir
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger the actual build.
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
cls.init(() => {
|
||||||
|
buildDocsInner()
|
||||||
|
.catch(rej)
|
||||||
|
.then(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
4
apps/build-docs/src/context.ts
Normal file
4
apps/build-docs/src/context.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default interface BuildContext {
|
||||||
|
gitRootDir: string;
|
||||||
|
baseDir: string;
|
||||||
|
}
|
||||||
28
apps/build-docs/src/frontend_script_entrypoint.ts
Normal file
28
apps/build-docs/src/frontend_script_entrypoint.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* The front script API is accessible to code notes with the "JS (frontend)" language.
|
||||||
|
*
|
||||||
|
* The entire API is exposed as a single global: {@link api}
|
||||||
|
*
|
||||||
|
* @module Frontend Script API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
||||||
|
* script note.
|
||||||
|
*
|
||||||
|
* Make sure to keep in line with frontend's `script_context.ts`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
|
||||||
|
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
|
||||||
|
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
|
||||||
|
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
|
||||||
|
export type { default as FNote } from "../../client/src/entities/fnote.js";
|
||||||
|
export type { Api } from "../../client/src/services/frontend_script_api.js";
|
||||||
|
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
|
||||||
|
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
|
||||||
|
|
||||||
|
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
export const api: Api = new FrontendScriptApi();
|
||||||
10
apps/build-docs/src/index.html
Normal file
10
apps/build-docs/src/index.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0; url=/user-guide">
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>If you are not redirected automatically, <a href="/user-guide">click here</a>.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
apps/build-docs/src/main.ts
Normal file
30
apps/build-docs/src/main.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { join } from "path";
|
||||||
|
import BuildContext from "./context";
|
||||||
|
import buildSwagger from "./swagger";
|
||||||
|
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
||||||
|
import buildDocs from "./build-docs";
|
||||||
|
import buildScriptApi from "./script-api";
|
||||||
|
|
||||||
|
const context: BuildContext = {
|
||||||
|
gitRootDir: join(__dirname, "../../../"),
|
||||||
|
baseDir: join(__dirname, "../../../site")
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Clean input dir.
|
||||||
|
if (existsSync(context.baseDir)) {
|
||||||
|
rmSync(context.baseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
mkdirSync(context.baseDir);
|
||||||
|
|
||||||
|
// Start building.
|
||||||
|
await buildDocs(context);
|
||||||
|
buildSwagger(context);
|
||||||
|
buildScriptApi(context);
|
||||||
|
|
||||||
|
// Copy index and 404 files.
|
||||||
|
cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html"));
|
||||||
|
cpSync(join(context.baseDir, "user-guide/404.html"), join(context.baseDir, "404.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
15
apps/build-docs/src/script-api.ts
Normal file
15
apps/build-docs/src/script-api.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import BuildContext from "./context";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
|
||||||
|
// Generate types
|
||||||
|
execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });
|
||||||
|
|
||||||
|
for (const config of [ "backend", "frontend" ]) {
|
||||||
|
const outDir = join(baseDir, "script-api", config);
|
||||||
|
execSync(`pnpm typedoc --options typedoc.${config}.json --html "${outDir}"`, {
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
32
apps/build-docs/src/swagger.ts
Normal file
32
apps/build-docs/src/swagger.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import BuildContext from "./context";
|
||||||
|
import { join } from "path";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { mkdirSync } from "fs";
|
||||||
|
|
||||||
|
interface BuildInfo {
|
||||||
|
specPath: string;
|
||||||
|
outDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIR_PREFIX = "rest-api";
|
||||||
|
|
||||||
|
const buildInfos: BuildInfo[] = [
|
||||||
|
{
|
||||||
|
// Paths are relative to Git root.
|
||||||
|
specPath: "apps/server/internal.openapi.yaml",
|
||||||
|
outDir: `${DIR_PREFIX}/internal`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
specPath: "apps/server/etapi.openapi.yaml",
|
||||||
|
outDir: `${DIR_PREFIX}/etapi`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
|
||||||
|
for (const { specPath, outDir } of buildInfos) {
|
||||||
|
const absSpecPath = join(gitRootDir, specPath);
|
||||||
|
const targetDir = join(baseDir, outDir);
|
||||||
|
mkdirSync(targetDir, { recursive: true });
|
||||||
|
execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
|
||||||
|
}
|
||||||
|
}
|
||||||
36
apps/build-docs/tsconfig.app.json
Normal file
36
apps/build-docs/tsconfig.app.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"target": "ES2020",
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": false,
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"express"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"../server/src/*.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"eslint.config.js",
|
||||||
|
"eslint.config.cjs",
|
||||||
|
"eslint.config.mjs"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../server/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../desktop/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../client/tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
apps/build-docs/tsconfig.json
Normal file
15
apps/build-docs/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../client"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10
apps/build-docs/typedoc.backend.json
Normal file
10
apps/build-docs/typedoc.backend.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://typedoc.org/schema.json",
|
||||||
|
"name": "Trilium Backend API",
|
||||||
|
"entryPoints": [
|
||||||
|
"src/backend_script_entrypoint.ts"
|
||||||
|
],
|
||||||
|
"plugin": [
|
||||||
|
"typedoc-plugin-missing-exports"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
apps/build-docs/typedoc.frontend.json
Normal file
10
apps/build-docs/typedoc.frontend.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://typedoc.org/schema.json",
|
||||||
|
"name": "Trilium Frontend API",
|
||||||
|
"entryPoints": [
|
||||||
|
"src/frontend_script_entrypoint.ts"
|
||||||
|
],
|
||||||
|
"plugin": [
|
||||||
|
"typedoc-plugin-missing-exports"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@
|
|||||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "9.38.0",
|
"@eslint/js": "9.39.1",
|
||||||
"@excalidraw/excalidraw": "0.18.0",
|
"@excalidraw/excalidraw": "0.18.0",
|
||||||
"@fullcalendar/core": "6.1.19",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
@ -37,12 +37,12 @@
|
|||||||
"bootstrap": "5.3.8",
|
"bootstrap": "5.3.8",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"color": "5.0.2",
|
"color": "5.0.2",
|
||||||
"dayjs": "1.11.18",
|
"dayjs": "1.11.19",
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
"debounce": "2.2.0",
|
"debounce": "3.0.0",
|
||||||
"draggabilly": "3.0.0",
|
"draggabilly": "3.0.0",
|
||||||
"force-graph": "1.51.0",
|
"force-graph": "1.51.0",
|
||||||
"globals": "16.4.0",
|
"globals": "16.5.0",
|
||||||
"i18next": "25.6.0",
|
"i18next": "25.6.0",
|
||||||
"i18next-http-backend": "3.0.2",
|
"i18next-http-backend": "3.0.2",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
@ -54,12 +54,12 @@
|
|||||||
"leaflet-gpx": "2.2.0",
|
"leaflet-gpx": "2.2.0",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "16.4.1",
|
"marked": "16.4.1",
|
||||||
"mermaid": "11.12.0",
|
"mermaid": "11.12.1",
|
||||||
"mind-elixir": "5.3.4",
|
"mind-elixir": "5.3.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"react-i18next": "16.2.0",
|
"react-i18next": "16.2.4",
|
||||||
"reveal.js": "5.2.1",
|
"reveal.js": "5.2.1",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.3.1",
|
"tabulator-tables": "6.3.1",
|
||||||
@ -76,7 +76,7 @@
|
|||||||
"@types/reveal.js": "5.2.1",
|
"@types/reveal.js": "5.2.1",
|
||||||
"@types/tabulator-tables": "6.3.0",
|
"@types/tabulator-tables": "6.3.0",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"happy-dom": "20.0.8",
|
"happy-dom": "20.0.10",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
"vite-plugin-static-copy": "3.1.4"
|
"vite-plugin-static-copy": "3.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { SqlExecuteResults } from "@triliumnext/commons";
|
|||||||
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
||||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||||
import { TypeWidget } from "../widgets/note_types.jsx";
|
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||||
@ -219,12 +219,12 @@ export type CommandMappings = {
|
|||||||
/** Works only in the electron context menu. */
|
/** Works only in the electron context menu. */
|
||||||
replaceMisspelling: CommandData;
|
replaceMisspelling: CommandData;
|
||||||
|
|
||||||
importMarkdownInline: CommandData;
|
|
||||||
showPasswordNotSet: CommandData;
|
showPasswordNotSet: CommandData;
|
||||||
showProtectedSessionPasswordDialog: CommandData;
|
showProtectedSessionPasswordDialog: CommandData;
|
||||||
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
||||||
showIncludeNoteDialog: CommandData & IncludeNoteOpts;
|
showIncludeNoteDialog: CommandData & IncludeNoteOpts;
|
||||||
showAddLinkDialog: CommandData & AddLinkOpts;
|
showAddLinkDialog: CommandData & AddLinkOpts;
|
||||||
|
showPasteMarkdownDialog: CommandData & MarkdownImportOpts;
|
||||||
closeProtectedSessionPasswordDialog: CommandData;
|
closeProtectedSessionPasswordDialog: CommandData;
|
||||||
copyImageReferenceToClipboard: CommandData;
|
copyImageReferenceToClipboard: CommandData;
|
||||||
copyImageToClipboard: CommandData;
|
copyImageToClipboard: CommandData;
|
||||||
@ -271,6 +271,7 @@ export type CommandMappings = {
|
|||||||
closeThisNoteSplit: CommandData;
|
closeThisNoteSplit: CommandData;
|
||||||
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
|
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
|
||||||
jumpToNote: CommandData;
|
jumpToNote: CommandData;
|
||||||
|
openTodayNote: CommandData;
|
||||||
commandPalette: CommandData;
|
commandPalette: CommandData;
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
|
|||||||
@ -159,6 +159,16 @@ export default class Entrypoints extends Component {
|
|||||||
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
|
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openTodayNoteCommand() {
|
||||||
|
const todayNote = await dateNoteService.getTodayNote();
|
||||||
|
if (!todayNote) {
|
||||||
|
console.warn("Missing today note.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await appContext.tabManager.openInSameTab(todayNote.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
async runActiveNoteCommand() {
|
async runActiveNoteCommand() {
|
||||||
const noteContext = appContext.tabManager.getActiveContext();
|
const noteContext = appContext.tabManager.getActiveContext();
|
||||||
if (!noteContext) {
|
if (!noteContext) {
|
||||||
|
|||||||
@ -417,7 +417,7 @@ export default class FNote {
|
|||||||
return notePaths;
|
return notePaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
|
getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] {
|
||||||
const isHoistedRoot = hoistedNoteId === "root";
|
const isHoistedRoot = hoistedNoteId === "root";
|
||||||
|
|
||||||
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
|
||||||
@ -428,7 +428,23 @@ export default class FNote {
|
|||||||
isHidden: path.includes("_hidden")
|
isHidden: path.includes("_hidden")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Calculate the length of the prefix match between two arrays
|
||||||
|
const prefixMatchLength = (path: string[], target: string[]) => {
|
||||||
|
const diffIndex = path.findIndex((seg, i) => seg !== target[i]);
|
||||||
|
return diffIndex === -1 ? Math.min(path.length, target.length) : diffIndex;
|
||||||
|
};
|
||||||
|
|
||||||
notePaths.sort((a, b) => {
|
notePaths.sort((a, b) => {
|
||||||
|
if (activeNotePath) {
|
||||||
|
const activeSegments = activeNotePath.split('/');
|
||||||
|
const aOverlap = prefixMatchLength(a.notePath, activeSegments);
|
||||||
|
const bOverlap = prefixMatchLength(b.notePath, activeSegments);
|
||||||
|
// Paths with more matching prefix segments are prioritized
|
||||||
|
// when the match count is equal, other criteria are used for sorting
|
||||||
|
if (bOverlap !== aOverlap) {
|
||||||
|
return bOverlap - aOverlap;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
|
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
|
||||||
return a.isInHoistedSubTree ? -1 : 1;
|
return a.isInHoistedSubTree ? -1 : 1;
|
||||||
} else if (a.isArchived !== b.isArchived) {
|
} else if (a.isArchived !== b.isArchived) {
|
||||||
@ -449,10 +465,11 @@ export default class FNote {
|
|||||||
* Returns the note path considered to be the "best"
|
* Returns the note path considered to be the "best"
|
||||||
*
|
*
|
||||||
* @param {string} [hoistedNoteId='root']
|
* @param {string} [hoistedNoteId='root']
|
||||||
|
* @param {string|null} [activeNotePath=null]
|
||||||
* @return {string[]} array of noteIds constituting the particular note path
|
* @return {string[]} array of noteIds constituting the particular note path
|
||||||
*/
|
*/
|
||||||
getBestNotePath(hoistedNoteId = "root") {
|
getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) {
|
||||||
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
|
return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
command: "editBranchPrefix",
|
command: "editBranchPrefix",
|
||||||
keyboardShortcut: "editBranchPrefix",
|
keyboardShortcut: "editBranchPrefix",
|
||||||
uiIcon: "bx bx-rename",
|
uiIcon: "bx bx-rename",
|
||||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
||||||
},
|
},
|
||||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
@import "boxicons/css/boxicons.min.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--print-font-size: 11pt;
|
--print-font-size: 11pt;
|
||||||
--ck-content-color-image-caption-background: transparent !important;
|
--ck-content-color-image-caption-background: transparent !important;
|
||||||
|
|||||||
@ -56,7 +56,20 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
|||||||
await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
|
await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
|
||||||
}
|
}
|
||||||
const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
|
const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
|
||||||
containerRef.current?.replaceChildren(...$renderedContent);
|
const container = containerRef.current!;
|
||||||
|
container.replaceChildren(...$renderedContent);
|
||||||
|
|
||||||
|
// Wait for all images to load.
|
||||||
|
const images = Array.from(container.querySelectorAll("img"));
|
||||||
|
await Promise.all(
|
||||||
|
images.map(img => {
|
||||||
|
if (img.complete) return Promise.resolve();
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
img.addEventListener("load", () => resolve(), { once: true });
|
||||||
|
img.addEventListener("error", () => resolve(), { once: true });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
load().then(() => requestAnimationFrame(onReady))
|
load().then(() => requestAnimationFrame(onReady))
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* The front script API is accessible to code notes with the "JS (frontend)" language.
|
|
||||||
*
|
|
||||||
* The entire API is exposed as a single global: {@link api}
|
|
||||||
*
|
|
||||||
* @module Frontend Script API
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
|
||||||
* script note.
|
|
||||||
*
|
|
||||||
* Make sure to keep in line with frontend's `script_context.ts`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type { default as BasicWidget } from "../widgets/basic_widget.js";
|
|
||||||
export type { default as FAttachment } from "../entities/fattachment.js";
|
|
||||||
export type { default as FAttribute } from "../entities/fattribute.js";
|
|
||||||
export type { default as FBranch } from "../entities/fbranch.js";
|
|
||||||
export type { default as FNote } from "../entities/fnote.js";
|
|
||||||
export type { Api } from "./frontend_script_api.js";
|
|
||||||
export type { default as NoteContextAwareWidget } from "../widgets/note_context_aware_widget.js";
|
|
||||||
export type { default as RightPanelWidget } from "../widgets/right_panel_widget.js";
|
|
||||||
|
|
||||||
import FrontendScriptApi, { type Api } from "./frontend_script_api.js";
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
export const api: Api = new FrontendScriptApi();
|
|
||||||
@ -20,9 +20,6 @@ function setupGlobs() {
|
|||||||
window.glob.froca = froca;
|
window.glob.froca = froca;
|
||||||
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
|
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
|
||||||
|
|
||||||
// for CKEditor integration (button on block toolbar)
|
|
||||||
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
|
|
||||||
|
|
||||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||||
const string = String(msg).toLowerCase();
|
const string = String(msg).toLowerCase();
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
|||||||
file: null,
|
file: null,
|
||||||
image: null,
|
image: null,
|
||||||
launcher: null,
|
launcher: null,
|
||||||
mermaid: null,
|
mermaid: "s1aBHPd79XYj",
|
||||||
mindMap: null,
|
mindMap: null,
|
||||||
noteMap: null,
|
noteMap: null,
|
||||||
relationMap: null,
|
relationMap: null,
|
||||||
|
|||||||
@ -159,7 +159,7 @@ describe("shortcuts", () => {
|
|||||||
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
||||||
|
|
||||||
// Special keys
|
// Special keys
|
||||||
for (const keyCode of [ "Delete", "Enter" ]) {
|
for (const keyCode of [ "Delete", "Enter", "NumpadEnter" ]) {
|
||||||
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
||||||
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
|
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,7 @@ for (let i = 1; i <= 19; i++) {
|
|||||||
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
||||||
"Delete",
|
"Delete",
|
||||||
"Enter",
|
"Enter",
|
||||||
|
"NumpadEnter",
|
||||||
...functionKeyCodes
|
...functionKeyCodes
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -26,21 +26,12 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
}
|
}
|
||||||
|
|
||||||
const path = notePath.split("/").reverse();
|
const path = notePath.split("/").reverse();
|
||||||
|
|
||||||
if (!path.includes("root")) {
|
|
||||||
path.push("root");
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectivePathSegments: string[] = [];
|
const effectivePathSegments: string[] = [];
|
||||||
let childNoteId: string | null = null;
|
let childNoteId: string | null = null;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (true) {
|
for (let i = 0; i < path.length; i++) {
|
||||||
if (i >= path.length) {
|
const parentNoteId = path[i];
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentNoteId = path[i++];
|
|
||||||
|
|
||||||
if (childNoteId !== null) {
|
if (childNoteId !== null) {
|
||||||
const child = await froca.getNote(childNoteId, !logErrors);
|
const child = await froca.getNote(childNoteId, !logErrors);
|
||||||
@ -65,7 +56,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parents.some((p) => p.noteId === parentNoteId)) {
|
if (!parents.some(p => p.noteId === parentNoteId) || (i === path.length - 1 && parentNoteId !== 'root')) {
|
||||||
if (logErrors) {
|
if (logErrors) {
|
||||||
const parent = froca.getNoteFromCache(parentNoteId);
|
const parent = froca.getNoteFromCache(parentNoteId);
|
||||||
|
|
||||||
@ -77,7 +68,8 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bestNotePath = child.getBestNotePath(hoistedNoteId);
|
const activeNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
const bestNotePath = child.getBestNotePath(hoistedNoteId, activeNotePath);
|
||||||
|
|
||||||
if (bestNotePath) {
|
if (bestNotePath) {
|
||||||
const pathToRoot = bestNotePath.reverse().slice(1);
|
const pathToRoot = bestNotePath.reverse().slice(1);
|
||||||
@ -108,7 +100,9 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
if (!note) {
|
if (!note) {
|
||||||
throw new Error(`Unable to find note: ${notePath}.`);
|
throw new Error(`Unable to find note: ${notePath}.`);
|
||||||
}
|
}
|
||||||
const bestNotePath = note.getBestNotePath(hoistedNoteId);
|
|
||||||
|
const activeNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
const bestNotePath = note.getBestNotePath(hoistedNoteId, activeNotePath);
|
||||||
|
|
||||||
if (!bestNotePath) {
|
if (!bestNotePath) {
|
||||||
throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);
|
throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);
|
||||||
|
|||||||
@ -11,7 +11,11 @@ export function reloadFrontendApp(reason?: string) {
|
|||||||
logInfo(`Frontend app reload: ${reason}`);
|
logInfo(`Frontend app reload: ${reason}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isElectron()) {
|
||||||
|
dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow()?.reload();
|
||||||
|
} else {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restartDesktopApp() {
|
export function restartDesktopApp() {
|
||||||
|
|||||||
@ -1,84 +0,0 @@
|
|||||||
import "normalize.css";
|
|
||||||
import "boxicons/css/boxicons.min.css";
|
|
||||||
import "@triliumnext/ckeditor5/src/theme/ck-content.css";
|
|
||||||
import "@triliumnext/share-theme/styles/index.css";
|
|
||||||
import "@triliumnext/share-theme/scripts/index.js";
|
|
||||||
|
|
||||||
async function ensureJQuery() {
|
|
||||||
const $ = (await import("jquery")).default;
|
|
||||||
(window as any).$ = $;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyMath() {
|
|
||||||
const anyMathBlock = document.querySelector("#content .math-tex");
|
|
||||||
if (!anyMathBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderMathInElement = (await import("./services/math.js")).renderMathInElement;
|
|
||||||
renderMathInElement(document.getElementById("content"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function formatCodeBlocks() {
|
|
||||||
const anyCodeBlock = document.querySelector("#content pre");
|
|
||||||
if (!anyCodeBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ensureJQuery();
|
|
||||||
const { formatCodeBlocks } = await import("./services/syntax_highlight.js");
|
|
||||||
await formatCodeBlocks($("#content"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupTextNote() {
|
|
||||||
formatCodeBlocks();
|
|
||||||
applyMath();
|
|
||||||
|
|
||||||
const setupMermaid = (await import("./share/mermaid.js")).default;
|
|
||||||
setupMermaid();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch note with given ID from backend
|
|
||||||
*
|
|
||||||
* @param noteId of the given note to be fetched. If false, fetches current note.
|
|
||||||
*/
|
|
||||||
async function fetchNote(noteId: string | null = null) {
|
|
||||||
if (!noteId) {
|
|
||||||
noteId = document.body.getAttribute("data-note-id");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await fetch(`api/notes/${noteId}`);
|
|
||||||
|
|
||||||
return await resp.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener(
|
|
||||||
"DOMContentLoaded",
|
|
||||||
() => {
|
|
||||||
const noteType = determineNoteType();
|
|
||||||
|
|
||||||
if (noteType === "text") {
|
|
||||||
setupTextNote();
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleMenuButton = document.getElementById("toggleMenuButton");
|
|
||||||
const layout = document.getElementById("layout");
|
|
||||||
|
|
||||||
if (toggleMenuButton && layout) {
|
|
||||||
toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
function determineNoteType() {
|
|
||||||
const bodyClass = document.body.className;
|
|
||||||
const match = bodyClass.match(/type-([^\s]+)/);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// workaround to prevent webpack from removing "fetchNote" as dead code:
|
|
||||||
// add fetchNote as property to the window object
|
|
||||||
Object.defineProperty(window, "fetchNote", {
|
|
||||||
value: fetchNote
|
|
||||||
});
|
|
||||||
@ -2034,9 +2034,9 @@ body.zen #right-pane,
|
|||||||
body.zen #mobile-sidebar-wrapper,
|
body.zen #mobile-sidebar-wrapper,
|
||||||
body.zen .tab-row-container,
|
body.zen .tab-row-container,
|
||||||
body.zen .tab-row-widget,
|
body.zen .tab-row-widget,
|
||||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget.visible)),
|
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
|
||||||
body.zen .ribbon-container:has(.classic-toolbar-widget.visible) .ribbon-top-row,
|
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
|
||||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget.visible)),
|
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
|
||||||
body.zen .note-icon-widget,
|
body.zen .note-icon-widget,
|
||||||
body.zen .title-row .icon-action,
|
body.zen .title-row .icon-action,
|
||||||
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
|
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
|
||||||
|
|||||||
@ -716,7 +716,6 @@
|
|||||||
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
|
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
|
||||||
},
|
},
|
||||||
"etapi": {
|
"etapi": {
|
||||||
"wiki": "ويكي",
|
|
||||||
"created": "تم الأنشاء",
|
"created": "تم الأنشاء",
|
||||||
"actions": "أجراءات",
|
"actions": "أجراءات",
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
"bulk_actions_executed": "批量操作已成功执行。",
|
"bulk_actions_executed": "批量操作已成功执行。",
|
||||||
"none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
|
"none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
|
||||||
"labels": "标签",
|
"labels": "标签",
|
||||||
"relations": "关联关系",
|
"relations": "关系",
|
||||||
"notes": "笔记",
|
"notes": "笔记",
|
||||||
"other": "其它"
|
"other": "其它"
|
||||||
},
|
},
|
||||||
@ -104,7 +104,8 @@
|
|||||||
"export_status": "导出状态",
|
"export_status": "导出状态",
|
||||||
"export_in_progress": "导出进行中:{{progressCount}}",
|
"export_in_progress": "导出进行中:{{progressCount}}",
|
||||||
"export_finished_successfully": "导出成功完成。",
|
"export_finished_successfully": "导出成功完成。",
|
||||||
"format_pdf": "PDF - 用于打印或共享目的。"
|
"format_pdf": "PDF - 用于打印或共享目的。",
|
||||||
|
"share-format": "HTML 网页发布——采用与共享笔记相同的主题,但可发布为静态网站。"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"noteNavigation": "笔记导航",
|
"noteNavigation": "笔记导航",
|
||||||
@ -184,7 +185,8 @@
|
|||||||
},
|
},
|
||||||
"import-status": "导入状态",
|
"import-status": "导入状态",
|
||||||
"in-progress": "导入进行中:{{progress}}",
|
"in-progress": "导入进行中:{{progress}}",
|
||||||
"successful": "导入成功完成。"
|
"successful": "导入成功完成。",
|
||||||
|
"importZipRecommendation": "导入 ZIP 文件时,笔记层级将反映压缩文件内的子目录结构。"
|
||||||
},
|
},
|
||||||
"include_note": {
|
"include_note": {
|
||||||
"dialog_title": "包含笔记",
|
"dialog_title": "包含笔记",
|
||||||
@ -259,7 +261,6 @@
|
|||||||
"delete_all_revisions": "删除此笔记的所有修订版本",
|
"delete_all_revisions": "删除此笔记的所有修订版本",
|
||||||
"delete_all_button": "删除所有修订版本",
|
"delete_all_button": "删除所有修订版本",
|
||||||
"help_title": "关于笔记修订版本的帮助",
|
"help_title": "关于笔记修订版本的帮助",
|
||||||
"revision_last_edited": "此修订版本上次编辑于 {{date}}",
|
|
||||||
"confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
|
"confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
|
||||||
"no_revisions": "此笔记暂无修订版本...",
|
"no_revisions": "此笔记暂无修订版本...",
|
||||||
"restore_button": "恢复",
|
"restore_button": "恢复",
|
||||||
@ -1288,10 +1289,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI 是一个 REST API,用于以编程方式访问 Trilium 实例,而无需 UI。",
|
"description": "ETAPI 是一个 REST API,用于以编程方式访问 Trilium 实例,而无需 UI。",
|
||||||
"see_more": "有关更多详细信息,请参见 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
|
|
||||||
"wiki": "维基",
|
|
||||||
"openapi_spec": "ETAPI OpenAPI 规范",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "创建新的 ETAPI 令牌",
|
"create_token": "创建新的 ETAPI 令牌",
|
||||||
"existing_tokens": "现有令牌",
|
"existing_tokens": "现有令牌",
|
||||||
"no_tokens_yet": "目前还没有令牌。点击上面的按钮创建一个。",
|
"no_tokens_yet": "目前还没有令牌。点击上面的按钮创建一个。",
|
||||||
@ -1558,7 +1555,9 @@
|
|||||||
"window-on-top": "保持此窗口置顶"
|
"window-on-top": "保持此窗口置顶"
|
||||||
},
|
},
|
||||||
"note_detail": {
|
"note_detail": {
|
||||||
"could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget"
|
"could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget",
|
||||||
|
"printing": "正在打印…",
|
||||||
|
"printing_pdf": "正在导出为PDF…"
|
||||||
},
|
},
|
||||||
"note_title": {
|
"note_title": {
|
||||||
"placeholder": "请输入笔记标题..."
|
"placeholder": "请输入笔记标题..."
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"homepage": "Startseite:",
|
"homepage": "Startseite:",
|
||||||
"app_version": "App-Version:",
|
"app_version": "App-Version:",
|
||||||
"db_version": "DB-Version:",
|
"db_version": "DB-Version:",
|
||||||
"sync_version": "Synch-version:",
|
"sync_version": "Sync-Version:",
|
||||||
"build_date": "Build-Datum:",
|
"build_date": "Build-Datum:",
|
||||||
"build_revision": "Build-Revision:",
|
"build_revision": "Build-Revision:",
|
||||||
"data_directory": "Datenverzeichnis:"
|
"data_directory": "Datenverzeichnis:"
|
||||||
@ -104,7 +104,8 @@
|
|||||||
"export_status": "Exportstatus",
|
"export_status": "Exportstatus",
|
||||||
"export_in_progress": "Export läuft: {{progressCount}}",
|
"export_in_progress": "Export läuft: {{progressCount}}",
|
||||||
"export_finished_successfully": "Der Export wurde erfolgreich abgeschlossen.",
|
"export_finished_successfully": "Der Export wurde erfolgreich abgeschlossen.",
|
||||||
"format_pdf": "PDF - für Ausdrucke oder Teilen."
|
"format_pdf": "PDF - für Ausdrucke oder Teilen.",
|
||||||
|
"share-format": "HTML für die Web-Veröffentlichung – verwendet dasselbe Theme wie bei freigegebenen Notizen, kann jedoch als statische Website veröffentlicht werden."
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"noteNavigation": "Notiz Navigation",
|
"noteNavigation": "Notiz Navigation",
|
||||||
@ -260,7 +261,6 @@
|
|||||||
"delete_all_revisions": "Lösche alle Revisionen dieser Notiz",
|
"delete_all_revisions": "Lösche alle Revisionen dieser Notiz",
|
||||||
"delete_all_button": "Alle Revisionen löschen",
|
"delete_all_button": "Alle Revisionen löschen",
|
||||||
"help_title": "Hilfe zu Notizrevisionen",
|
"help_title": "Hilfe zu Notizrevisionen",
|
||||||
"revision_last_edited": "Diese Revision wurde zuletzt am {{date}} bearbeitet",
|
|
||||||
"confirm_delete_all": "Möchtest du alle Revisionen dieser Notiz löschen?",
|
"confirm_delete_all": "Möchtest du alle Revisionen dieser Notiz löschen?",
|
||||||
"no_revisions": "Für diese Notiz gibt es noch keine Revisionen...",
|
"no_revisions": "Für diese Notiz gibt es noch keine Revisionen...",
|
||||||
"confirm_restore": "Möchtest du diese Revision wiederherstellen? Dadurch werden der aktuelle Titel und Inhalt der Notiz mit dieser Revision überschrieben.",
|
"confirm_restore": "Möchtest du diese Revision wiederherstellen? Dadurch werden der aktuelle Titel und Inhalt der Notiz mit dieser Revision überschrieben.",
|
||||||
@ -991,7 +991,7 @@
|
|||||||
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
|
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
|
||||||
"start_session_button": "Starte eine geschützte Sitzung",
|
"start_session_button": "Starte eine geschützte Sitzung",
|
||||||
"started": "Geschützte Sitzung gestartet.",
|
"started": "Geschützte Sitzung gestartet.",
|
||||||
"wrong_password": "Passwort flasch.",
|
"wrong_password": "Passwort falsch.",
|
||||||
"protecting-finished-successfully": "Geschützt erfolgreich beendet.",
|
"protecting-finished-successfully": "Geschützt erfolgreich beendet.",
|
||||||
"unprotecting-finished-successfully": "Ungeschützt erfolgreich beendet.",
|
"unprotecting-finished-successfully": "Ungeschützt erfolgreich beendet.",
|
||||||
"protecting-in-progress": "Schützen läuft: {{count}}",
|
"protecting-in-progress": "Schützen läuft: {{count}}",
|
||||||
@ -1286,10 +1286,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI ist eine REST-API, die für den programmgesteuerten Zugriff auf die Trilium-Instanz ohne Benutzeroberfläche verwendet wird.",
|
"description": "ETAPI ist eine REST-API, die für den programmgesteuerten Zugriff auf die Trilium-Instanz ohne Benutzeroberfläche verwendet wird.",
|
||||||
"see_more": "Weitere Details können im {{- link_to_wiki}} und in der {{- link_to_openapi_spec}} oder der {{- link_to_swagger_ui }} gefunden werden.",
|
|
||||||
"wiki": "Wiki",
|
|
||||||
"openapi_spec": "ETAPI OpenAPI-Spezifikation",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "Erstelle ein neues ETAPI-Token",
|
"create_token": "Erstelle ein neues ETAPI-Token",
|
||||||
"existing_tokens": "Vorhandene Token",
|
"existing_tokens": "Vorhandene Token",
|
||||||
"no_tokens_yet": "Es sind noch keine Token vorhanden. Klicke auf die Schaltfläche oben, um eine zu erstellen.",
|
"no_tokens_yet": "Es sind noch keine Token vorhanden. Klicke auf die Schaltfläche oben, um eine zu erstellen.",
|
||||||
@ -1658,7 +1654,7 @@
|
|||||||
"add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen",
|
"add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen",
|
||||||
"cut": "Ausschneiden",
|
"cut": "Ausschneiden",
|
||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
"copy-link": "Link opieren",
|
"copy-link": "Link kopieren",
|
||||||
"paste": "Einfügen",
|
"paste": "Einfügen",
|
||||||
"paste-as-plain-text": "Als unformatierten Text einfügen",
|
"paste-as-plain-text": "Als unformatierten Text einfügen",
|
||||||
"search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten"
|
"search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten"
|
||||||
|
|||||||
@ -36,10 +36,13 @@
|
|||||||
},
|
},
|
||||||
"branch_prefix": {
|
"branch_prefix": {
|
||||||
"edit_branch_prefix": "Edit branch prefix",
|
"edit_branch_prefix": "Edit branch prefix",
|
||||||
|
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
|
||||||
"help_on_tree_prefix": "Help on Tree prefix",
|
"help_on_tree_prefix": "Help on Tree prefix",
|
||||||
"prefix": "Prefix: ",
|
"prefix": "Prefix: ",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"branch_prefix_saved": "Branch prefix has been saved."
|
"branch_prefix_saved": "Branch prefix has been saved.",
|
||||||
|
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
|
||||||
|
"affected_branches": "Affected branches ({{count}}):"
|
||||||
},
|
},
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
"bulk_actions": "Bulk actions",
|
"bulk_actions": "Bulk actions",
|
||||||
@ -104,7 +107,8 @@
|
|||||||
"export_status": "Export status",
|
"export_status": "Export status",
|
||||||
"export_in_progress": "Export in progress: {{progressCount}}",
|
"export_in_progress": "Export in progress: {{progressCount}}",
|
||||||
"export_finished_successfully": "Export finished successfully.",
|
"export_finished_successfully": "Export finished successfully.",
|
||||||
"format_pdf": "PDF - for printing or sharing purposes."
|
"format_pdf": "PDF - for printing or sharing purposes.",
|
||||||
|
"share-format": "HTML for web publishing - uses the same theme that is used shared notes, but can be published as a static website."
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"title": "Cheatsheet",
|
"title": "Cheatsheet",
|
||||||
@ -260,7 +264,6 @@
|
|||||||
"delete_all_revisions": "Delete all revisions of this note",
|
"delete_all_revisions": "Delete all revisions of this note",
|
||||||
"delete_all_button": "Delete all revisions",
|
"delete_all_button": "Delete all revisions",
|
||||||
"help_title": "Help on Note Revisions",
|
"help_title": "Help on Note Revisions",
|
||||||
"revision_last_edited": "This revision was last edited on {{date}}",
|
|
||||||
"confirm_delete_all": "Do you want to delete all revisions of this note?",
|
"confirm_delete_all": "Do you want to delete all revisions of this note?",
|
||||||
"no_revisions": "No revisions for this note yet...",
|
"no_revisions": "No revisions for this note yet...",
|
||||||
"restore_button": "Restore",
|
"restore_button": "Restore",
|
||||||
@ -1453,10 +1456,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI is a REST API used to access Trilium instance programmatically, without UI.",
|
"description": "ETAPI is a REST API used to access Trilium instance programmatically, without UI.",
|
||||||
"see_more": "See more details in the {{- link_to_wiki}} and the {{- link_to_openapi_spec}} or the {{- link_to_swagger_ui }}.",
|
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "ETAPI OpenAPI spec",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "Create new ETAPI token",
|
"create_token": "Create new ETAPI token",
|
||||||
"existing_tokens": "Existing tokens",
|
"existing_tokens": "Existing tokens",
|
||||||
"no_tokens_yet": "There are no tokens yet. Click on the button above to create one.",
|
"no_tokens_yet": "There are no tokens yet. Click on the button above to create one.",
|
||||||
@ -2038,6 +2037,9 @@
|
|||||||
"start-presentation": "Start presentation",
|
"start-presentation": "Start presentation",
|
||||||
"slide-overview": "Toggle an overview of the slides"
|
"slide-overview": "Toggle an overview of the slides"
|
||||||
},
|
},
|
||||||
|
"calendar_view": {
|
||||||
|
"delete_note": "Delete note..."
|
||||||
|
},
|
||||||
"command_palette": {
|
"command_palette": {
|
||||||
"tree-action-name": "Tree: {{name}}",
|
"tree-action-name": "Tree: {{name}}",
|
||||||
"export_note_title": "Export Note",
|
"export_note_title": "Export Note",
|
||||||
|
|||||||
@ -104,7 +104,8 @@
|
|||||||
"export_status": "Estado de exportación",
|
"export_status": "Estado de exportación",
|
||||||
"export_in_progress": "Exportación en curso: {{progressCount}}",
|
"export_in_progress": "Exportación en curso: {{progressCount}}",
|
||||||
"export_finished_successfully": "La exportación finalizó exitosamente.",
|
"export_finished_successfully": "La exportación finalizó exitosamente.",
|
||||||
"format_pdf": "PDF - para propósitos de impresión o compartición."
|
"format_pdf": "PDF - para propósitos de impresión o compartición.",
|
||||||
|
"share-format": "HTML para publicación web: utiliza el mismo tema que se utiliza en las notas compartidas, pero se puede publicar como un sitio web estático."
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"noteNavigation": "Navegación de notas",
|
"noteNavigation": "Navegación de notas",
|
||||||
@ -184,7 +185,8 @@
|
|||||||
},
|
},
|
||||||
"import-status": "Estado de importación",
|
"import-status": "Estado de importación",
|
||||||
"in-progress": "Importación en progreso: {{progress}}",
|
"in-progress": "Importación en progreso: {{progress}}",
|
||||||
"successful": "Importación finalizada exitosamente."
|
"successful": "Importación finalizada exitosamente.",
|
||||||
|
"importZipRecommendation": "Al importar un archivo ZIP, la jerarquía de notas reflejará la estructura de subdirectorios dentro del archivo comprimido."
|
||||||
},
|
},
|
||||||
"include_note": {
|
"include_note": {
|
||||||
"dialog_title": "Incluir nota",
|
"dialog_title": "Incluir nota",
|
||||||
@ -259,7 +261,6 @@
|
|||||||
"delete_all_revisions": "Eliminar todas las revisiones de esta nota",
|
"delete_all_revisions": "Eliminar todas las revisiones de esta nota",
|
||||||
"delete_all_button": "Eliminar todas las revisiones",
|
"delete_all_button": "Eliminar todas las revisiones",
|
||||||
"help_title": "Ayuda sobre revisiones de notas",
|
"help_title": "Ayuda sobre revisiones de notas",
|
||||||
"revision_last_edited": "Esta revisión se editó por última vez en {{date}}",
|
|
||||||
"confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
|
"confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
|
||||||
"no_revisions": "Aún no hay revisiones para esta nota...",
|
"no_revisions": "Aún no hay revisiones para esta nota...",
|
||||||
"restore_button": "Restaurar",
|
"restore_button": "Restaurar",
|
||||||
@ -1445,10 +1446,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI es una REST API que se utiliza para acceder a la instancia de Trilium mediante programación, sin interfaz de usuario.",
|
"description": "ETAPI es una REST API que se utiliza para acceder a la instancia de Trilium mediante programación, sin interfaz de usuario.",
|
||||||
"see_more": "Véa más detalles en el {{- link_to_wiki}} y el {{- link_to_openapi_spec}} o el {{- link_to_swagger_ui }}.",
|
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "Especificación ETAPI OpenAPI",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "Crear nuevo token ETAPI",
|
"create_token": "Crear nuevo token ETAPI",
|
||||||
"existing_tokens": "Tokens existentes",
|
"existing_tokens": "Tokens existentes",
|
||||||
"no_tokens_yet": "Aún no hay tokens. Dé clic en el botón de arriba para crear uno.",
|
"no_tokens_yet": "Aún no hay tokens. Dé clic en el botón de arriba para crear uno.",
|
||||||
@ -1715,7 +1712,9 @@
|
|||||||
"window-on-top": "Mantener esta ventana en la parte superior"
|
"window-on-top": "Mantener esta ventana en la parte superior"
|
||||||
},
|
},
|
||||||
"note_detail": {
|
"note_detail": {
|
||||||
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'"
|
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
|
||||||
|
"printing": "Impresión en curso...",
|
||||||
|
"printing_pdf": "Exportando a PDF en curso.."
|
||||||
},
|
},
|
||||||
"note_title": {
|
"note_title": {
|
||||||
"placeholder": "escriba el título de la nota aquí..."
|
"placeholder": "escriba el título de la nota aquí..."
|
||||||
|
|||||||
@ -260,7 +260,6 @@
|
|||||||
"delete_all_revisions": "Supprimer toutes les versions de cette note",
|
"delete_all_revisions": "Supprimer toutes les versions de cette note",
|
||||||
"delete_all_button": "Supprimer toutes les versions",
|
"delete_all_button": "Supprimer toutes les versions",
|
||||||
"help_title": "Aide sur les versions de notes",
|
"help_title": "Aide sur les versions de notes",
|
||||||
"revision_last_edited": "Cette version a été modifiée pour la dernière fois le {{date}}",
|
|
||||||
"confirm_delete_all": "Voulez-vous supprimer toutes les versions de cette note ?",
|
"confirm_delete_all": "Voulez-vous supprimer toutes les versions de cette note ?",
|
||||||
"no_revisions": "Aucune version pour cette note pour l'instant...",
|
"no_revisions": "Aucune version pour cette note pour l'instant...",
|
||||||
"confirm_restore": "Voulez-vous restaurer cette version ? Le titre et le contenu actuels de la note seront écrasés par cette version.",
|
"confirm_restore": "Voulez-vous restaurer cette version ? Le titre et le contenu actuels de la note seront écrasés par cette version.",
|
||||||
@ -1289,8 +1288,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI est une API REST utilisée pour accéder à l'instance Trilium par programme, sans interface utilisateur.",
|
"description": "ETAPI est une API REST utilisée pour accéder à l'instance Trilium par programme, sans interface utilisateur.",
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "Spec ETAPI OpenAPI",
|
|
||||||
"create_token": "Créer un nouveau jeton ETAPI",
|
"create_token": "Créer un nouveau jeton ETAPI",
|
||||||
"existing_tokens": "Jetons existants",
|
"existing_tokens": "Jetons existants",
|
||||||
"no_tokens_yet": "Il n'y a pas encore de jetons. Cliquez sur le bouton ci-dessus pour en créer un.",
|
"no_tokens_yet": "Il n'y a pas encore de jetons. Cliquez sur le bouton ci-dessus pour en créer un.",
|
||||||
@ -1307,9 +1304,7 @@
|
|||||||
"delete_token": "Supprimer/désactiver ce token",
|
"delete_token": "Supprimer/désactiver ce token",
|
||||||
"rename_token_title": "Renommer le jeton",
|
"rename_token_title": "Renommer le jeton",
|
||||||
"rename_token_message": "Veuillez saisir le nom du nouveau jeton",
|
"rename_token_message": "Veuillez saisir le nom du nouveau jeton",
|
||||||
"delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?",
|
"delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?"
|
||||||
"see_more": "Voir plus de détails dans le {{- link_to_wiki}} et le {{- link_to_openapi_spec}} ou le {{- link_to_swagger_ui }}.",
|
|
||||||
"swagger_ui": "Interface utilisateur ETAPI Swagger"
|
|
||||||
},
|
},
|
||||||
"options_widget": {
|
"options_widget": {
|
||||||
"options_status": "Statut des options",
|
"options_status": "Statut des options",
|
||||||
|
|||||||
5
apps/client/src/translations/hi/translation.json
Normal file
5
apps/client/src/translations/hi/translation.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"about": {
|
||||||
|
"title": "ट्रिलियम नोट्स के बारें में"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -109,7 +109,8 @@
|
|||||||
"export_type_single": "Solo questa nota, senza le sottostanti",
|
"export_type_single": "Solo questa nota, senza le sottostanti",
|
||||||
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
|
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
|
||||||
"opml_version_1": "OPML v.1.0 - solo testo semplice",
|
"opml_version_1": "OPML v.1.0 - solo testo semplice",
|
||||||
"opml_version_2": "OPML v2.0 - supporta anche HTML"
|
"opml_version_2": "OPML v2.0 - supporta anche HTML",
|
||||||
|
"share-format": "HTML per la pubblicazione sul web - utilizza lo stesso tema utilizzato per le note condivise, ma può essere pubblicato come sito web statico."
|
||||||
},
|
},
|
||||||
"password_not_set": {
|
"password_not_set": {
|
||||||
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
|
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
|
||||||
@ -132,10 +133,6 @@
|
|||||||
"new_token_message": "Inserisci il nome del nuovo token",
|
"new_token_message": "Inserisci il nome del nuovo token",
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI è un'API REST utilizzata per accedere alle istanze di Trilium in modo programmatico, senza interfaccia utente.",
|
"description": "ETAPI è un'API REST utilizzata per accedere alle istanze di Trilium in modo programmatico, senza interfaccia utente.",
|
||||||
"see_more": "Per maggiori dettagli consulta {{- link_to_wiki}} e {{- link_to_openapi_spec}} o {{- link_to_swagger_ui}}.",
|
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "Specifiche ETAPI OpenAPI",
|
|
||||||
"swagger_ui": "Interfaccia utente ETAPI Swagger",
|
|
||||||
"create_token": "Crea un nuovo token ETAPI",
|
"create_token": "Crea un nuovo token ETAPI",
|
||||||
"existing_tokens": "Token esistenti",
|
"existing_tokens": "Token esistenti",
|
||||||
"no_tokens_yet": "Non ci sono ancora token. Clicca sul pulsante qui sopra per crearne uno.",
|
"no_tokens_yet": "Non ci sono ancora token. Clicca sul pulsante qui sopra per crearne uno.",
|
||||||
@ -867,7 +864,6 @@
|
|||||||
"delete_all_revisions": "Elimina tutte le revisioni di questa nota",
|
"delete_all_revisions": "Elimina tutte le revisioni di questa nota",
|
||||||
"delete_all_button": "Elimina tutte le revisioni",
|
"delete_all_button": "Elimina tutte le revisioni",
|
||||||
"help_title": "Aiuto sulle revisioni delle note",
|
"help_title": "Aiuto sulle revisioni delle note",
|
||||||
"revision_last_edited": "Questa revisione è stata modificata l'ultima volta il {{date}}",
|
|
||||||
"confirm_delete_all": "Vuoi eliminare tutte le revisioni di questa nota?",
|
"confirm_delete_all": "Vuoi eliminare tutte le revisioni di questa nota?",
|
||||||
"no_revisions": "Ancora nessuna revisione per questa nota...",
|
"no_revisions": "Ancora nessuna revisione per questa nota...",
|
||||||
"restore_button": "Ripristina",
|
"restore_button": "Ripristina",
|
||||||
|
|||||||
@ -254,7 +254,8 @@
|
|||||||
"export_status": "エクスポート状況",
|
"export_status": "エクスポート状況",
|
||||||
"export_in_progress": "エクスポート処理中: {{progressCount}}",
|
"export_in_progress": "エクスポート処理中: {{progressCount}}",
|
||||||
"export_finished_successfully": "エクスポートが正常に完了しました。",
|
"export_finished_successfully": "エクスポートが正常に完了しました。",
|
||||||
"format_pdf": "PDF - 印刷または共有目的に。"
|
"format_pdf": "PDF - 印刷または共有目的に。",
|
||||||
|
"share-format": "Web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 Web サイトとして公開できます。"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"title": "チートシート",
|
"title": "チートシート",
|
||||||
@ -610,7 +611,6 @@
|
|||||||
"delete_all_revisions": "このノートの変更履歴をすべて削除",
|
"delete_all_revisions": "このノートの変更履歴をすべて削除",
|
||||||
"delete_all_button": "変更履歴をすべて削除",
|
"delete_all_button": "変更履歴をすべて削除",
|
||||||
"help_title": "変更履歴のヘルプ",
|
"help_title": "変更履歴のヘルプ",
|
||||||
"revision_last_edited": "この変更は{{date}}に行われました",
|
|
||||||
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
|
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
|
||||||
"no_revisions": "このノートに変更履歴はまだありません...",
|
"no_revisions": "このノートに変更履歴はまだありません...",
|
||||||
"restore_button": "復元",
|
"restore_button": "復元",
|
||||||
@ -657,10 +657,6 @@
|
|||||||
"created": "作成日時",
|
"created": "作成日時",
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI は、Trilium インスタンスに UI なしでプログラム的にアクセスするための REST API です。",
|
"description": "ETAPI は、Trilium インスタンスに UI なしでプログラム的にアクセスするための REST API です。",
|
||||||
"see_more": "詳細は{{- link_to_wiki}}と{{- link_to_openapi_spec}}または{{- link_to_swagger_ui }}を参照してください。",
|
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "ETAPI OpenAPIの仕様",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "新しくETAPIトークンを作成",
|
"create_token": "新しくETAPIトークンを作成",
|
||||||
"existing_tokens": "既存のトークン",
|
"existing_tokens": "既存のトークン",
|
||||||
"no_tokens_yet": "トークンはまだありません。上のボタンをクリックして作成してください。",
|
"no_tokens_yet": "トークンはまだありません。上のボタンをクリックして作成してください。",
|
||||||
|
|||||||
@ -13,6 +13,13 @@
|
|||||||
"critical-error": {
|
"critical-error": {
|
||||||
"title": "Kritische Error",
|
"title": "Kritische Error",
|
||||||
"message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
|
"message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
|
||||||
|
},
|
||||||
|
"widget-error": {
|
||||||
|
"title": "Starten widget mislukt",
|
||||||
|
"message-unknown": "Onbekende widget kan niet gestart worden omdat:\n\n{{message}}"
|
||||||
|
},
|
||||||
|
"bundle-error": {
|
||||||
|
"title": "Custom script laden mislukt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"add_link": {
|
"add_link": {
|
||||||
|
|||||||
@ -912,7 +912,6 @@
|
|||||||
"delete_all_revisions": "Usuń wszystkie wersje tej notatki",
|
"delete_all_revisions": "Usuń wszystkie wersje tej notatki",
|
||||||
"delete_all_button": "Usuń wszystkie wersje",
|
"delete_all_button": "Usuń wszystkie wersje",
|
||||||
"help_title": "Pomoc dotycząca wersji notatki",
|
"help_title": "Pomoc dotycząca wersji notatki",
|
||||||
"revision_last_edited": "Ta wersja była ostatnio edytowana {{date}}",
|
|
||||||
"confirm_delete_all": "Czy chcesz usunąć wszystkie wersje tej notatki?",
|
"confirm_delete_all": "Czy chcesz usunąć wszystkie wersje tej notatki?",
|
||||||
"no_revisions": "Brak wersji dla tej notatki...",
|
"no_revisions": "Brak wersji dla tej notatki...",
|
||||||
"restore_button": "Przywróć",
|
"restore_button": "Przywróć",
|
||||||
@ -1664,10 +1663,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI to interfejs API REST używany do programowego dostępu do instancji Trilium, bez interfejsu użytkownika.",
|
"description": "ETAPI to interfejs API REST używany do programowego dostępu do instancji Trilium, bez interfejsu użytkownika.",
|
||||||
"see_more": "Zobacz więcej szczegółów w {{- link_to_wiki}} oraz w {{- link_to_openapi_spec}} lub {{- link_to_swagger_ui }}.",
|
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "specyfikacja ETAPI OpenAPI",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "Utwórz nowy token ETAPI",
|
"create_token": "Utwórz nowy token ETAPI",
|
||||||
"existing_tokens": "Istniejące tokeny",
|
"existing_tokens": "Istniejące tokeny",
|
||||||
"no_tokens_yet": "Nie ma jeszcze żadnych tokenów. Kliknij przycisk powyżej, aby utworzyć jeden.",
|
"no_tokens_yet": "Nie ma jeszcze żadnych tokenów. Kliknij przycisk powyżej, aby utworzyć jeden.",
|
||||||
|
|||||||
@ -259,7 +259,6 @@
|
|||||||
"delete_all_revisions": "Apagar todas as versões desta nota",
|
"delete_all_revisions": "Apagar todas as versões desta nota",
|
||||||
"delete_all_button": "Apagar todas as versões",
|
"delete_all_button": "Apagar todas as versões",
|
||||||
"help_title": "Ajuda sobre as versões da nota",
|
"help_title": "Ajuda sobre as versões da nota",
|
||||||
"revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
|
|
||||||
"confirm_delete_all": "Quer apagar todas as versões desta nota?",
|
"confirm_delete_all": "Quer apagar todas as versões desta nota?",
|
||||||
"no_revisions": "Ainda não há versões para esta nota...",
|
"no_revisions": "Ainda não há versões para esta nota...",
|
||||||
"restore_button": "Recuperar",
|
"restore_button": "Recuperar",
|
||||||
@ -1423,10 +1422,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI é uma API REST usada para aceder a instância do Trilium programaticamente, sem interface gráfica.",
|
"description": "ETAPI é uma API REST usada para aceder a instância do Trilium programaticamente, sem interface gráfica.",
|
||||||
"see_more": "Veja mais pormenores no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
|
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "Especificação OpenAPI do ETAPI",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "Criar token ETAPI",
|
"create_token": "Criar token ETAPI",
|
||||||
"existing_tokens": "Tokens existentes",
|
"existing_tokens": "Tokens existentes",
|
||||||
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
|
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
|
||||||
|
|||||||
@ -415,7 +415,6 @@
|
|||||||
"delete_all_revisions": "Excluir todas as versões desta nota",
|
"delete_all_revisions": "Excluir todas as versões desta nota",
|
||||||
"delete_all_button": "Excluir todas as versões",
|
"delete_all_button": "Excluir todas as versões",
|
||||||
"help_title": "Ajuda sobre as versões da nota",
|
"help_title": "Ajuda sobre as versões da nota",
|
||||||
"revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
|
|
||||||
"confirm_delete_all": "Você quer excluir todas as versões desta nota?",
|
"confirm_delete_all": "Você quer excluir todas as versões desta nota?",
|
||||||
"no_revisions": "Ainda não há versões para esta nota...",
|
"no_revisions": "Ainda não há versões para esta nota...",
|
||||||
"restore_button": "Recuperar",
|
"restore_button": "Recuperar",
|
||||||
@ -1933,10 +1932,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.",
|
"description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.",
|
||||||
"see_more": "Veja mais detalhes no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
|
|
||||||
"wiki": "wiki",
|
|
||||||
"openapi_spec": "Especificação OpenAPI do ETAPI",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "Criar novo token ETAPI",
|
"create_token": "Criar novo token ETAPI",
|
||||||
"existing_tokens": "Tokens existentes",
|
"existing_tokens": "Tokens existentes",
|
||||||
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
|
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
|
||||||
|
|||||||
@ -507,17 +507,13 @@
|
|||||||
"new_token_message": "Introduceți denumirea noului token",
|
"new_token_message": "Introduceți denumirea noului token",
|
||||||
"new_token_title": "Token ETAPI nou",
|
"new_token_title": "Token ETAPI nou",
|
||||||
"no_tokens_yet": "Nu există încă token-uri. Clic pe butonul de deasupra pentru a crea una.",
|
"no_tokens_yet": "Nu există încă token-uri. Clic pe butonul de deasupra pentru a crea una.",
|
||||||
"openapi_spec": "Specificația OpenAPI pentru ETAPI",
|
|
||||||
"swagger_ui": "UI-ul Swagger pentru ETAPI",
|
|
||||||
"rename_token": "Redenumește token-ul",
|
"rename_token": "Redenumește token-ul",
|
||||||
"rename_token_message": "Introduceți denumirea noului token",
|
"rename_token_message": "Introduceți denumirea noului token",
|
||||||
"rename_token_title": "Redenumire token",
|
"rename_token_title": "Redenumire token",
|
||||||
"see_more": "Vedeți mai multe detalii în {{- link_to_wiki}} și în {{- link_to_openapi_spec}} sau în {{- link_to_swagger_ui }}.",
|
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"token_created_message": "Copiați token-ul creat în clipboard. Trilium stochează token-ul ca hash așadar această valoare poate fi văzută doar acum.",
|
"token_created_message": "Copiați token-ul creat în clipboard. Trilium stochează token-ul ca hash așadar această valoare poate fi văzută doar acum.",
|
||||||
"token_created_title": "Token ETAPI creat",
|
"token_created_title": "Token ETAPI creat",
|
||||||
"token_name": "Denumire token",
|
"token_name": "Denumire token"
|
||||||
"wiki": "wiki"
|
|
||||||
},
|
},
|
||||||
"execute_script": {
|
"execute_script": {
|
||||||
"example_1": "De exemplu, pentru a adăuga un șir de caractere la titlul unei notițe, se poate folosi acest mic script:",
|
"example_1": "De exemplu, pentru a adăuga un șir de caractere la titlul unei notițe, se poate folosi acest mic script:",
|
||||||
@ -1090,7 +1086,6 @@
|
|||||||
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
|
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
|
||||||
"restore_button": "Restaurează",
|
"restore_button": "Restaurează",
|
||||||
"revision_deleted": "Revizia notiței a fost ștearsă.",
|
"revision_deleted": "Revizia notiței a fost ștearsă.",
|
||||||
"revision_last_edited": "Revizia a fost ultima oară modificată pe {{date}}",
|
|
||||||
"revision_restored": "Revizia notiței a fost restaurată.",
|
"revision_restored": "Revizia notiței a fost restaurată.",
|
||||||
"revisions_deleted": "Notița reviziei a fost ștearsă.",
|
"revisions_deleted": "Notița reviziei a fost ștearsă.",
|
||||||
"maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.",
|
"maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.",
|
||||||
|
|||||||
@ -366,7 +366,6 @@
|
|||||||
"delete_all_button": "Удалить все версии",
|
"delete_all_button": "Удалить все версии",
|
||||||
"help_title": "Помощь по версиям заметок",
|
"help_title": "Помощь по версиям заметок",
|
||||||
"confirm_delete_all": "Вы хотите удалить все версии этой заметки?",
|
"confirm_delete_all": "Вы хотите удалить все версии этой заметки?",
|
||||||
"revision_last_edited": "Эта версия последний раз редактировалась {{date}}",
|
|
||||||
"confirm_restore": "Хотите восстановить эту версию? Текущее название и содержание заметки будут перезаписаны этой версией.",
|
"confirm_restore": "Хотите восстановить эту версию? Текущее название и содержание заметки будут перезаписаны этой версией.",
|
||||||
"confirm_delete": "Вы хотите удалить эту версию?",
|
"confirm_delete": "Вы хотите удалить эту версию?",
|
||||||
"revisions_deleted": "Версии заметки были удалены.",
|
"revisions_deleted": "Версии заметки были удалены.",
|
||||||
@ -1441,7 +1440,6 @@
|
|||||||
},
|
},
|
||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"wiki": "вики",
|
|
||||||
"created": "Создано",
|
"created": "Создано",
|
||||||
"actions": "Действия",
|
"actions": "Действия",
|
||||||
"existing_tokens": "Существующие токены",
|
"existing_tokens": "Существующие токены",
|
||||||
@ -1449,10 +1447,7 @@
|
|||||||
"default_token_name": "новый токен",
|
"default_token_name": "новый токен",
|
||||||
"rename_token_title": "Переименовать токен",
|
"rename_token_title": "Переименовать токен",
|
||||||
"description": "ETAPI — это REST API, используемый для программного доступа к экземпляру Trilium без пользовательского интерфейса.",
|
"description": "ETAPI — это REST API, используемый для программного доступа к экземпляру Trilium без пользовательского интерфейса.",
|
||||||
"see_more": "Более подробную информацию смотрите в {{- link_to_wiki}} и {{- link_to_openapi_spec}} или {{- link_to_swagger_ui }}.",
|
|
||||||
"create_token": "Создать новый токен ETAPI",
|
"create_token": "Создать новый токен ETAPI",
|
||||||
"openapi_spec": "Спецификация ETAPI OpenAPI",
|
|
||||||
"swagger_ui": "Пользовательский интерфейс ETAPI Swagger",
|
|
||||||
"new_token_title": "Новый токен ETAPI",
|
"new_token_title": "Новый токен ETAPI",
|
||||||
"token_created_title": "Создан токен ETAPI",
|
"token_created_title": "Создан токен ETAPI",
|
||||||
"rename_token": "Переименовать этот токен",
|
"rename_token": "Переименовать этот токен",
|
||||||
|
|||||||
@ -256,7 +256,6 @@
|
|||||||
"delete_all_revisions": "Obriši sve revizije ove beleške",
|
"delete_all_revisions": "Obriši sve revizije ove beleške",
|
||||||
"delete_all_button": "Obriši sve revizije",
|
"delete_all_button": "Obriši sve revizije",
|
||||||
"help_title": "Pomoć za Revizije beleški",
|
"help_title": "Pomoć za Revizije beleški",
|
||||||
"revision_last_edited": "Ova revizija je poslednji put izmenjena {{date}}",
|
|
||||||
"confirm_delete_all": "Da li želite da obrišete sve revizije ove beleške?",
|
"confirm_delete_all": "Da li želite da obrišete sve revizije ove beleške?",
|
||||||
"no_revisions": "Još uvek nema revizija za ovu belešku...",
|
"no_revisions": "Još uvek nema revizija za ovu belešku...",
|
||||||
"restore_button": "Vrati",
|
"restore_button": "Vrati",
|
||||||
|
|||||||
@ -104,7 +104,8 @@
|
|||||||
"export_in_progress": "正在匯出:{{progressCount}}",
|
"export_in_progress": "正在匯出:{{progressCount}}",
|
||||||
"export_finished_successfully": "成功匯出。",
|
"export_finished_successfully": "成功匯出。",
|
||||||
"format_html": "HTML - 推薦,因為它保留了所有格式",
|
"format_html": "HTML - 推薦,因為它保留了所有格式",
|
||||||
"format_pdf": "PDF - 用於列印或與他人分享。"
|
"format_pdf": "PDF - 用於列印或與他人分享。",
|
||||||
|
"share-format": "HTML 網頁發佈——使用與共享筆記相同的佈景主題,但可發佈為靜態網站。"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"noteNavigation": "筆記導航",
|
"noteNavigation": "筆記導航",
|
||||||
@ -260,7 +261,6 @@
|
|||||||
"delete_all_revisions": "刪除此筆記的所有歷史版本",
|
"delete_all_revisions": "刪除此筆記的所有歷史版本",
|
||||||
"delete_all_button": "刪除所有歷史版本",
|
"delete_all_button": "刪除所有歷史版本",
|
||||||
"help_title": "關於筆記歷史版本的說明",
|
"help_title": "關於筆記歷史版本的說明",
|
||||||
"revision_last_edited": "此歷史版本上次於 {{date}} 編輯",
|
|
||||||
"confirm_delete_all": "您是否要刪除此筆記的所有歷史版本?",
|
"confirm_delete_all": "您是否要刪除此筆記的所有歷史版本?",
|
||||||
"no_revisions": "此筆記暫無歷史版本…",
|
"no_revisions": "此筆記暫無歷史版本…",
|
||||||
"confirm_restore": "您是否要還原此歷史版本?這將使用此歷史版本覆寫筆記的目前標題和內容。",
|
"confirm_restore": "您是否要還原此歷史版本?這將使用此歷史版本覆寫筆記的目前標題和內容。",
|
||||||
@ -1281,8 +1281,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI 是一個 REST API,用於以編程方式訪問 Trilium 實例,而無需 UI。",
|
"description": "ETAPI 是一個 REST API,用於以編程方式訪問 Trilium 實例,而無需 UI。",
|
||||||
"wiki": "維基",
|
|
||||||
"openapi_spec": "ETAPI OpenAPI 規範",
|
|
||||||
"create_token": "新增 ETAPI 令牌",
|
"create_token": "新增 ETAPI 令牌",
|
||||||
"existing_tokens": "現有令牌",
|
"existing_tokens": "現有令牌",
|
||||||
"no_tokens_yet": "目前還沒有令牌。點擊上面的按鈕新增一個。",
|
"no_tokens_yet": "目前還沒有令牌。點擊上面的按鈕新增一個。",
|
||||||
@ -1299,9 +1297,7 @@
|
|||||||
"delete_token": "刪除 / 停用此令牌",
|
"delete_token": "刪除 / 停用此令牌",
|
||||||
"rename_token_title": "重新命名令牌",
|
"rename_token_title": "重新命名令牌",
|
||||||
"rename_token_message": "請輸入新的令牌名稱",
|
"rename_token_message": "請輸入新的令牌名稱",
|
||||||
"delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?",
|
"delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?"
|
||||||
"see_more": "有關更多詳細資訊,請參閱 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI"
|
|
||||||
},
|
},
|
||||||
"options_widget": {
|
"options_widget": {
|
||||||
"options_status": "選項狀態",
|
"options_status": "選項狀態",
|
||||||
|
|||||||
@ -309,7 +309,6 @@
|
|||||||
"delete_all_revisions": "Видалити всі версії цієї нотатки",
|
"delete_all_revisions": "Видалити всі версії цієї нотатки",
|
||||||
"delete_all_button": "Видалити всі версії",
|
"delete_all_button": "Видалити всі версії",
|
||||||
"help_title": "Довідка щодо Версій нотаток",
|
"help_title": "Довідка щодо Версій нотаток",
|
||||||
"revision_last_edited": "Цю версію востаннє редагували {{date}}",
|
|
||||||
"confirm_delete_all": "Ви хочете видалити всі версії цієї нотатки?",
|
"confirm_delete_all": "Ви хочете видалити всі версії цієї нотатки?",
|
||||||
"no_revisions": "Поки що немає версій цієї нотатки...",
|
"no_revisions": "Поки що немає версій цієї нотатки...",
|
||||||
"restore_button": "Відновити",
|
"restore_button": "Відновити",
|
||||||
@ -1403,10 +1402,6 @@
|
|||||||
"etapi": {
|
"etapi": {
|
||||||
"title": "ETAPI",
|
"title": "ETAPI",
|
||||||
"description": "ETAPI — це REST API, який використовується для програмного доступу до екземпляра Trilium без інтерфейсу користувача.",
|
"description": "ETAPI — це REST API, який використовується для програмного доступу до екземпляра Trilium без інтерфейсу користувача.",
|
||||||
"see_more": "Див. докладнішу інформацію у {{- link_to_wiki}} та {{- link_to_openapi_spec}} або {{- link_to_swagger_ui }}.",
|
|
||||||
"wiki": "вікі",
|
|
||||||
"openapi_spec": "ETAPI OpenAPI spec",
|
|
||||||
"swagger_ui": "ETAPI Swagger UI",
|
|
||||||
"create_token": "Створити новий токен ETAPI",
|
"create_token": "Створити новий токен ETAPI",
|
||||||
"existing_tokens": "Існуючі токени",
|
"existing_tokens": "Існуючі токени",
|
||||||
"no_tokens_yet": "Токенів поки що немає. Натисніть кнопку вище, щоб створити його.",
|
"no_tokens_yet": "Токенів поки що немає. Натисніть кнопку вище, щоб створити його.",
|
||||||
|
|||||||
1
apps/client/src/types.d.ts
vendored
1
apps/client/src/types.d.ts
vendored
@ -26,7 +26,6 @@ interface CustomGlobals {
|
|||||||
appContext: AppContext;
|
appContext: AppContext;
|
||||||
froca: Froca;
|
froca: Froca;
|
||||||
treeCache: Froca;
|
treeCache: Froca;
|
||||||
importMarkdownInline: () => Promise<unknown>;
|
|
||||||
SEARCH_HELP_TEXT: string;
|
SEARCH_HELP_TEXT: string;
|
||||||
activeDialog: JQuery<HTMLElement> | null;
|
activeDialog: JQuery<HTMLElement> | null;
|
||||||
componentId: string;
|
componentId: string;
|
||||||
|
|||||||
28
apps/client/src/widgets/collections/calendar/context_menu.ts
Normal file
28
apps/client/src/widgets/collections/calendar/context_menu.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import FNote from "../../../entities/fnote";
|
||||||
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
|
import branches from "../../../services/branches";
|
||||||
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
|
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
contextMenu.show({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
items: [
|
||||||
|
...link_context_menu.getItems(),
|
||||||
|
{ kind: "separator" },
|
||||||
|
{
|
||||||
|
title: t("calendar_view.delete_note"),
|
||||||
|
uiIcon: "bx bx-trash",
|
||||||
|
handler: async () => {
|
||||||
|
const branchId = parentNote.childToBranch[noteId];
|
||||||
|
await branches.deleteNotes([ branchId ], false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import Button, { ButtonGroup } from "../../react/Button";
|
|||||||
import ActionButton from "../../react/ActionButton";
|
import ActionButton from "../../react/ActionButton";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||||
|
import { openCalendarContextMenu } from "./context_menu";
|
||||||
|
|
||||||
interface CalendarViewData {
|
interface CalendarViewData {
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
|||||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
const { eventDidMount } = useEventDisplayCustomization();
|
const { eventDidMount } = useEventDisplayCustomization(note);
|
||||||
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
||||||
|
|
||||||
// React to changes.
|
// React to changes.
|
||||||
@ -253,7 +254,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEventDisplayCustomization() {
|
function useEventDisplayCustomization(parentNote: FNote) {
|
||||||
const eventDidMount = useCallback((e: EventMountArg) => {
|
const eventDidMount = useCallback((e: EventMountArg) => {
|
||||||
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
||||||
|
|
||||||
@ -302,6 +303,11 @@ function useEventDisplayCustomization() {
|
|||||||
}
|
}
|
||||||
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
|
||||||
|
const noteId = e.event.extendedProps.noteId;
|
||||||
|
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
return { eventDidMount };
|
return { eventDidMount };
|
||||||
}
|
}
|
||||||
|
|||||||
13
apps/client/src/widgets/dialogs/branch_prefix.css
Normal file
13
apps/client/src/widgets/dialogs/branch_prefix.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.branch-prefix-dialog .branch-prefix-notes-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-prefix-dialog .branch-prefix-notes-list ul {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-prefix-dialog .branch-prefix-current {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
@ -10,14 +10,26 @@ import Button from "../react/Button.jsx";
|
|||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
import FBranch from "../../entities/fbranch.js";
|
import FBranch from "../../entities/fbranch.js";
|
||||||
|
import type { ContextMenuCommandData } from "../../components/app_context.js";
|
||||||
|
import "./branch_prefix.css";
|
||||||
|
|
||||||
|
// Virtual branches (e.g., from search results) start with this prefix
|
||||||
|
const VIRTUAL_BRANCH_PREFIX = "virt-";
|
||||||
|
|
||||||
export default function BranchPrefixDialog() {
|
export default function BranchPrefixDialog() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const [ branch, setBranch ] = useState<FBranch>();
|
const [ branches, setBranches ] = useState<FBranch[]>([]);
|
||||||
const [ prefix, setPrefix ] = useState("");
|
const [ prefix, setPrefix ] = useState("");
|
||||||
const branchInput = useRef<HTMLInputElement>(null);
|
const branchInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useTriliumEvent("editBranchPrefix", async () => {
|
useTriliumEvent("editBranchPrefix", async (data?: ContextMenuCommandData) => {
|
||||||
|
let branchIds: string[] = [];
|
||||||
|
|
||||||
|
if (data?.selectedOrActiveBranchIds && data.selectedOrActiveBranchIds.length > 0) {
|
||||||
|
// Multi-select mode from tree context menu
|
||||||
|
branchIds = data.selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith(VIRTUAL_BRANCH_PREFIX));
|
||||||
|
} else {
|
||||||
|
// Single branch mode from keyboard shortcut or when no selection
|
||||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
if (!notePath) {
|
if (!notePath) {
|
||||||
return;
|
return;
|
||||||
@ -29,8 +41,8 @@ export default function BranchPrefixDialog() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
|
const branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||||
if (!newBranchId) {
|
if (!branchId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentNote = await froca.getNote(parentNoteId);
|
const parentNote = await froca.getNote(parentNoteId);
|
||||||
@ -38,25 +50,46 @@ export default function BranchPrefixDialog() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBranch = froca.getBranch(newBranchId);
|
branchIds = [branchId];
|
||||||
setBranch(newBranch);
|
}
|
||||||
setPrefix(newBranch?.prefix ?? "");
|
|
||||||
|
if (branchIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBranches = branchIds
|
||||||
|
.map(id => froca.getBranch(id))
|
||||||
|
.filter((branch): branch is FBranch => branch !== null);
|
||||||
|
|
||||||
|
if (newBranches.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBranches(newBranches);
|
||||||
|
// Use the prefix of the first branch as the initial value
|
||||||
|
setPrefix(newBranches[0]?.prefix ?? "");
|
||||||
setShown(true);
|
setShown(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (!branch) {
|
if (branches.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
savePrefix(branch.branchId, prefix);
|
if (branches.length === 1) {
|
||||||
|
await savePrefix(branches[0].branchId, prefix);
|
||||||
|
} else {
|
||||||
|
await savePrefixBatch(branches.map(b => b.branchId), prefix);
|
||||||
|
}
|
||||||
setShown(false);
|
setShown(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSingleBranch = branches.length === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="branch-prefix-dialog"
|
className="branch-prefix-dialog"
|
||||||
title={t("branch_prefix.edit_branch_prefix")}
|
title={isSingleBranch ? t("branch_prefix.edit_branch_prefix") : t("branch_prefix.edit_branch_prefix_multiple", { count: branches.length })}
|
||||||
size="lg"
|
size="lg"
|
||||||
onShown={() => branchInput.current?.focus()}
|
onShown={() => branchInput.current?.focus()}
|
||||||
onHidden={() => setShown(false)}
|
onHidden={() => setShown(false)}
|
||||||
@ -69,9 +102,27 @@ export default function BranchPrefixDialog() {
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
||||||
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
||||||
<div class="branch-prefix-note-title input-group-text"> - {branch && branch.getNoteFromCache().title}</div>
|
{isSingleBranch && branches[0] && (
|
||||||
|
<div class="branch-prefix-note-title input-group-text"> - {branches[0].getNoteFromCache().title}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
{!isSingleBranch && (
|
||||||
|
<div className="branch-prefix-notes-list">
|
||||||
|
<strong>{t("branch_prefix.affected_branches", { count: branches.length })}</strong>
|
||||||
|
<ul>
|
||||||
|
{branches.map((branch) => {
|
||||||
|
const note = branch.getNoteFromCache();
|
||||||
|
return (
|
||||||
|
<li key={branch.branchId}>
|
||||||
|
{branch.prefix && <span className="branch-prefix-current">{branch.prefix} - </span>}
|
||||||
|
{note.title}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -80,3 +131,8 @@ async function savePrefix(branchId: string, prefix: string) {
|
|||||||
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
||||||
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function savePrefixBatch(branchIds: string[], prefix: string) {
|
||||||
|
await server.put("branches/set-prefix-batch", { branchIds, prefix });
|
||||||
|
toast.showMessage(t("branch_prefix.branch_prefix_saved_multiple", { count: branchIds.length }));
|
||||||
|
}
|
||||||
|
|||||||
@ -79,6 +79,7 @@ export default function ExportDialog() {
|
|||||||
values={[
|
values={[
|
||||||
{ value: "html", label: t("export.format_html_zip") },
|
{ value: "html", label: t("export.format_html_zip") },
|
||||||
{ value: "markdown", label: t("export.format_markdown") },
|
{ value: "markdown", label: t("export.format_markdown") },
|
||||||
|
{ value: "share", label: t("export.share-format") },
|
||||||
{ value: "opml", label: t("export.format_opml") }
|
{ value: "opml", label: t("export.format_opml") }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
import appContext from "../../components/app_context";
|
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
import toast from "../../services/toast";
|
import toast from "../../services/toast";
|
||||||
@ -7,6 +6,12 @@ import utils from "../../services/utils";
|
|||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
import EditableTextTypeWidget from "../type_widgets/editable_text";
|
||||||
|
import { CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog";
|
||||||
|
|
||||||
|
export interface MarkdownImportOpts {
|
||||||
|
editorApi: CKEditorApi;
|
||||||
|
}
|
||||||
|
|
||||||
interface RenderMarkdownResponse {
|
interface RenderMarkdownResponse {
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
@ -14,39 +19,36 @@ interface RenderMarkdownResponse {
|
|||||||
|
|
||||||
export default function MarkdownImportDialog() {
|
export default function MarkdownImportDialog() {
|
||||||
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
|
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const editorApiRef = useRef<CKEditorApi>(null);
|
||||||
|
const [ textTypeWidget, setTextTypeWidget ] = useState<EditableTextTypeWidget>();
|
||||||
const [ text, setText ] = useState("");
|
const [ text, setText ] = useState("");
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
|
|
||||||
const triggerImport = useCallback(() => {
|
useTriliumEvent("showPasteMarkdownDialog", ({ editorApi }) => {
|
||||||
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
|
setTextTypeWidget(textTypeWidget);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
const { clipboard } = utils.dynamicRequire("electron");
|
const { clipboard } = utils.dynamicRequire("electron");
|
||||||
const text = clipboard.readText();
|
const text = clipboard.readText();
|
||||||
|
|
||||||
convertMarkdownToHtml(text);
|
convertMarkdownToHtml(text, editorApi);
|
||||||
} else {
|
} else {
|
||||||
|
editorApiRef.current = editorApi;
|
||||||
setShown(true);
|
setShown(true);
|
||||||
}
|
}
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
useTriliumEvent("importMarkdownInline", triggerImport);
|
|
||||||
useTriliumEvent("pasteMarkdownIntoText", triggerImport);
|
|
||||||
|
|
||||||
async function sendForm() {
|
|
||||||
await convertMarkdownToHtml(text);
|
|
||||||
setText("");
|
|
||||||
setShown(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="markdown-import-dialog" title={t("markdown_import.dialog_title")} size="lg"
|
className="markdown-import-dialog" title={t("markdown_import.dialog_title")} size="lg"
|
||||||
footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={sendForm} keyboardShortcut="Ctrl+Space" />}
|
footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={() => setShown(false)} keyboardShortcut="Ctrl+Enter" />}
|
||||||
onShown={() => markdownImportTextArea.current?.focus()}
|
onShown={() => markdownImportTextArea.current?.focus()}
|
||||||
onHidden={() => setShown(false) }
|
onHidden={async () => {
|
||||||
|
if (editorApiRef.current) {
|
||||||
|
await convertMarkdownToHtml(text, editorApiRef.current);
|
||||||
|
}
|
||||||
|
setShown(false);
|
||||||
|
setText("");
|
||||||
|
}}
|
||||||
show={shown}
|
show={shown}
|
||||||
>
|
>
|
||||||
<p>{t("markdown_import.modal_body_text")}</p>
|
<p>{t("markdown_import.modal_body_text")}</p>
|
||||||
@ -56,26 +58,15 @@ export default function MarkdownImportDialog() {
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && e.ctrlKey) {
|
if (e.key === "Enter" && e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendForm();
|
setShown(false);
|
||||||
}
|
}
|
||||||
}}></textarea>
|
}}></textarea>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertMarkdownToHtml(markdownContent: string) {
|
async function convertMarkdownToHtml(markdownContent: string, textTypeWidget: CKEditorApi) {
|
||||||
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
|
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
|
||||||
|
textTypeWidget.addHtmlToEditor(htmlContent);
|
||||||
const textEditor = await appContext.tabManager.getActiveContext()?.getTextEditor();
|
|
||||||
if (!textEditor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewFragment = textEditor.data.processor.toView(htmlContent);
|
|
||||||
const modelFragment = textEditor.data.toModel(viewFragment);
|
|
||||||
|
|
||||||
textEditor.model.insertContent(modelFragment, textEditor.model.document.selection);
|
|
||||||
textEditor.editing.view.focus();
|
|
||||||
|
|
||||||
toast.showMessage(t("markdown_import.import_success"));
|
toast.showMessage(t("markdown_import.import_success"));
|
||||||
}
|
}
|
||||||
@ -154,6 +154,11 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid not showing recent notes when creating a new empty tab.
|
||||||
|
if ("noteContext" in data && data.noteContext.ntxId !== "_popup-editor") {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
return super.handleEventInChildren(name, data);
|
return super.handleEventInChildren(name, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -140,11 +140,10 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
|
|||||||
<FormList onSelect={onSelect} fullHeight>
|
<FormList onSelect={onSelect} fullHeight>
|
||||||
{revisions.map((item) =>
|
{revisions.map((item) =>
|
||||||
<FormListItem
|
<FormListItem
|
||||||
title={t("revisions.revision_last_edited", { date: item.dateLastEdited })}
|
|
||||||
value={item.revisionId}
|
value={item.revisionId}
|
||||||
active={currentRevision && item.revisionId === currentRevision.revisionId}
|
active={currentRevision && item.revisionId === currentRevision.revisionId}
|
||||||
>
|
>
|
||||||
{item.dateLastEdited && item.dateLastEdited.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
|
{item.dateCreated && item.dateCreated.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
|
||||||
</FormListItem>
|
</FormListItem>
|
||||||
)}
|
)}
|
||||||
</FormList>);
|
</FormList>);
|
||||||
|
|||||||
@ -1591,6 +1591,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
this.clearSelectedNodes();
|
this.clearSelectedNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async editBranchPrefixCommand({ node }: CommandListenerData<"editBranchPrefix">) {
|
||||||
|
const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-"));
|
||||||
|
|
||||||
|
if (!branchIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the event with the selected branch IDs
|
||||||
|
appContext.triggerEvent("editBranchPrefix", {
|
||||||
|
selectedOrActiveBranchIds: branchIds,
|
||||||
|
node: node
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
|
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
|
||||||
if (node.data.noteId === "root") {
|
if (node.data.noteId === "root") {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export default function EditedNotesTab({ note }: TabContext) {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}))}
|
}), " ")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
|
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
|
||||||
|
|||||||
@ -264,7 +264,6 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-end: 5px;
|
inset-inline-end: 5px;
|
||||||
bottom: 5px;
|
bottom: 5px;
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.style-resolver {
|
.style-resolver {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { ComponentChildren } from "preact";
|
|||||||
import { CSSProperties } from "preact/compat";
|
import { CSSProperties } from "preact/compat";
|
||||||
|
|
||||||
interface OptionsSectionProps {
|
interface OptionsSectionProps {
|
||||||
title?: string;
|
title?: ComponentChildren;
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
noCard?: boolean;
|
noCard?: boolean;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import dialog from "../../../services/dialog";
|
|||||||
import { formatDateTime } from "../../../utils/formatters";
|
import { formatDateTime } from "../../../utils/formatters";
|
||||||
import ActionButton from "../../react/ActionButton";
|
import ActionButton from "../../react/ActionButton";
|
||||||
import { useTriliumEvent } from "../../react/hooks";
|
import { useTriliumEvent } from "../../react/hooks";
|
||||||
|
import HelpButton from "../../react/HelpButton";
|
||||||
|
|
||||||
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
|
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
|
||||||
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
|
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
|
||||||
@ -53,14 +54,8 @@ export default function EtapiSettings() {
|
|||||||
return (
|
return (
|
||||||
<OptionsSection title={t("etapi.title")}>
|
<OptionsSection title={t("etapi.title")}>
|
||||||
<FormText>
|
<FormText>
|
||||||
{t("etapi.description")}<br />
|
{t("etapi.description")}
|
||||||
<RawHtml
|
<HelpButton helpPage="pgxEVkzLl1OP" />
|
||||||
html={t("etapi.see_more", {
|
|
||||||
link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
|
|
||||||
// TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
|
|
||||||
link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
|
|
||||||
link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
|
|
||||||
})} />
|
|
||||||
</FormText>
|
</FormText>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -68,6 +63,7 @@ export default function EtapiSettings() {
|
|||||||
text={t("etapi.create_token")}
|
text={t("etapi.create_token")}
|
||||||
onClick={createTokenCallback}
|
onClick={createTokenCallback}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>{t("etapi.existing_tokens")}</h5>
|
<h5>{t("etapi.existing_tokens")}</h5>
|
||||||
|
|||||||
@ -72,8 +72,8 @@ function EditorFeatures() {
|
|||||||
return (
|
return (
|
||||||
<OptionsSection title={t("editorfeatures.title")}>
|
<OptionsSection title={t("editorfeatures.title")}>
|
||||||
<EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
<EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
||||||
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.note_completion_description")} />
|
||||||
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.slash_commands_description")} />
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export interface CKEditorApi {
|
|||||||
getSelectedText(): string;
|
getSelectedText(): string;
|
||||||
addLink(notePath: string, linkTitle: string | null, externalLink?: boolean): void;
|
addLink(notePath: string, linkTitle: string | null, externalLink?: boolean): void;
|
||||||
addLinkToEditor(linkHref: string, linkTitle: string): void;
|
addLinkToEditor(linkHref: string, linkTitle: string): void;
|
||||||
|
addHtmlToEditor(html: string): void;
|
||||||
addIncludeNote(noteId: string, boxSize?: BoxSize): void;
|
addIncludeNote(noteId: string, boxSize?: BoxSize): void;
|
||||||
addImage(noteId: string): Promise<void>;
|
addImage(noteId: string): Promise<void>;
|
||||||
}
|
}
|
||||||
@ -100,6 +101,26 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
addHtmlToEditor(html: string) {
|
||||||
|
const editor = watchdogRef.current?.editor;
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
editor.model.change((writer) => {
|
||||||
|
const viewFragment = editor.data.processor.toView(html);
|
||||||
|
const modelFragment = editor.data.toModel(viewFragment);
|
||||||
|
const insertPosition = editor.model.document.selection.getLastPosition();
|
||||||
|
|
||||||
|
if (insertPosition) {
|
||||||
|
const range = editor.model.insertContent(modelFragment, insertPosition);
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
writer.setSelection(range.end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.editing.view.focus();
|
||||||
|
},
|
||||||
async addImage(noteId) {
|
async addImage(noteId) {
|
||||||
const editor = watchdogRef.current?.editor;
|
const editor = watchdogRef.current?.editor;
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
|||||||
@ -90,6 +90,12 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
pasteMarkdownIntoTextCommand() {
|
||||||
|
if (!editorApiRef.current) return;
|
||||||
|
parentComponent?.triggerCommand("showPasteMarkdownDialog", {
|
||||||
|
editorApi: editorApiRef.current,
|
||||||
|
});
|
||||||
|
},
|
||||||
// Include note functionality note
|
// Include note functionality note
|
||||||
addIncludeNoteToTextCommand() {
|
addIncludeNoteToTextCommand() {
|
||||||
if (!editorApiRef.current) return;
|
if (!editorApiRef.current) return;
|
||||||
|
|||||||
@ -74,7 +74,6 @@ export default defineConfig(() => ({
|
|||||||
mobile: join(__dirname, "src", "mobile.ts"),
|
mobile: join(__dirname, "src", "mobile.ts"),
|
||||||
login: join(__dirname, "src", "login.ts"),
|
login: join(__dirname, "src", "login.ts"),
|
||||||
setup: join(__dirname, "src", "setup.ts"),
|
setup: join(__dirname, "src", "setup.ts"),
|
||||||
share: join(__dirname, "src", "share.ts"),
|
|
||||||
set_password: join(__dirname, "src", "set_password.ts"),
|
set_password: join(__dirname, "src", "set_password.ts"),
|
||||||
runtime: join(__dirname, "src", "runtime.ts"),
|
runtime: join(__dirname, "src", "runtime.ts"),
|
||||||
print: join(__dirname, "src", "print.tsx")
|
print: join(__dirname, "src", "print.tsx")
|
||||||
@ -84,7 +83,8 @@ export default defineConfig(() => ({
|
|||||||
chunkFileNames: "src/[name].js",
|
chunkFileNames: "src/[name].js",
|
||||||
assetFileNames: "src/[name].[ext]",
|
assetFileNames: "src/[name].[ext]",
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
"ckeditor5": [ "@triliumnext/ckeditor5" ]
|
"ckeditor5": [ "@triliumnext/ckeditor5" ],
|
||||||
|
"boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onwarn(warning, rollupWarn) {
|
onwarn(warning, rollupWarn) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
|
|||||||
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
|
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
|
||||||
|
|
||||||
:POWERSHELL
|
:POWERSHELL
|
||||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:NODE_TLS_REJECT_UNAUTHORIZED -Value 0; ./trilium.exe"
|
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:NODE_TLS_REJECT_UNAUTHORIZED -Value 0; ./trilium.exe"
|
||||||
GOTO END
|
GOTO END
|
||||||
|
|
||||||
:BATCH
|
:BATCH
|
||||||
|
|||||||
@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
|
|||||||
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
|
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
|
||||||
|
|
||||||
:POWERSHELL
|
:POWERSHELL
|
||||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; ./trilium.exe"
|
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; ./trilium.exe"
|
||||||
GOTO END
|
GOTO END
|
||||||
|
|
||||||
:BATCH
|
:BATCH
|
||||||
|
|||||||
@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
|
|||||||
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
|
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
|
||||||
|
|
||||||
:POWERSHELL
|
:POWERSHELL
|
||||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:TRILIUM_SAFE_MODE -Value 1; ./trilium.exe --disable-gpu"
|
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_SAFE_MODE -Value 1; ./trilium.exe --disable-gpu"
|
||||||
GOTO END
|
GOTO END
|
||||||
|
|
||||||
:BATCH
|
:BATCH
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
"@triliumnext/commons": "workspace:*",
|
"@triliumnext/commons": "workspace:*",
|
||||||
"@triliumnext/server": "workspace:*",
|
"@triliumnext/server": "workspace:*",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.4.0",
|
"electron": "38.5.0",
|
||||||
"@electron-forge/cli": "7.10.2",
|
"@electron-forge/cli": "7.10.2",
|
||||||
"@electron-forge/maker-deb": "7.10.2",
|
"@electron-forge/maker-deb": "7.10.2",
|
||||||
"@electron-forge/maker-dmg": "7.10.2",
|
"@electron-forge/maker-dmg": "7.10.2",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ async function main() {
|
|||||||
// Copy assets.
|
// Copy assets.
|
||||||
build.copy("src/assets", "assets/");
|
build.copy("src/assets", "assets/");
|
||||||
build.copy("/apps/server/src/assets", "assets/");
|
build.copy("/apps/server/src/assets", "assets/");
|
||||||
|
build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
|
||||||
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
|
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
|
||||||
|
|
||||||
// Copy node modules dependencies
|
// Copy node modules dependencies
|
||||||
|
|||||||
3
apps/edit-docs/demo/!!!meta.json
vendored
3
apps/edit-docs/demo/!!!meta.json
vendored
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 2,
|
"formatVersion": 2,
|
||||||
"appVersion": "0.99.2",
|
"appVersion": "0.99.3",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"isClone": false,
|
"isClone": false,
|
||||||
@ -2700,6 +2700,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "html",
|
"format": "html",
|
||||||
|
"dataFileName": "Note Types.html",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"dirFileName": "Note Types",
|
"dirFileName": "Note Types",
|
||||||
"children": [
|
"children": [
|
||||||
|
|||||||
2
apps/edit-docs/demo/navigation.html
vendored
2
apps/edit-docs/demo/navigation.html
vendored
@ -270,7 +270,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Note Types
|
<li><a href="root/Trilium%20Demo/Note%20Types.html" target="detail">Note Types</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a>
|
<li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<div class="ck-content">
|
<div class="ck-content">
|
||||||
<h2>☑️ Tasks</h2>
|
<h2>☑️ Tasks</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
|
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
16
apps/edit-docs/demo/root/Trilium Demo.html
vendored
16
apps/edit-docs/demo/root/Trilium Demo.html
vendored
@ -14,11 +14,10 @@
|
|||||||
|
|
||||||
<div class="ck-content">
|
<div class="ck-content">
|
||||||
<figure class="image image-style-align-right image_resized" style="width:29.84%;">
|
<figure class="image image-style-align-right image_resized" style="width:29.84%;">
|
||||||
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg" width="150"
|
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg"
|
||||||
height="150">
|
width="150" height="150">
|
||||||
</figure>
|
</figure>
|
||||||
<p><strong>Welcome to Trilium Notes!</strong>
|
<p><strong>Welcome to Trilium Notes!</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p>This is a "demo" document packaged with Trilium to showcase some of its
|
<p>This is a "demo" document packaged with Trilium to showcase some of its
|
||||||
features and also give you some ideas on how you might structure your notes.
|
features and also give you some ideas on how you might structure your notes.
|
||||||
@ -26,22 +25,17 @@
|
|||||||
you wish.</p>
|
you wish.</p>
|
||||||
<p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or
|
<p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or
|
||||||
our <a href="https://github.com/TriliumNext">GitHub repository</a>
|
our <a href="https://github.com/TriliumNext">GitHub repository</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<h2>Cleanup</h2>
|
<h2>Cleanup</h2>
|
||||||
|
|
||||||
<p>Once you're finished with experimenting and want to cleanup these pages,
|
<p>Once you're finished with experimenting and want to cleanup these pages,
|
||||||
you can simply delete them all.</p>
|
you can simply delete them all.</p>
|
||||||
<h2>Formatting</h2>
|
<h2>Formatting</h2>
|
||||||
|
|
||||||
<p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
|
<p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
|
||||||
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or
|
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or
|
||||||
<a
|
<a
|
||||||
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
|
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
|
||||||
<h3>Lists</h3>
|
<h3>Lists</h3>
|
||||||
|
|
||||||
<p><strong>Ordered:</strong>
|
<p><strong>Ordered:</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
|
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
|
||||||
@ -56,7 +50,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p><strong>Unordered:</strong>
|
<p><strong>Unordered:</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
|
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
|
||||||
@ -67,7 +60,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Block quotes</h3>
|
<h3>Block quotes</h3>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>Whereof one cannot speak, thereof one must be silent”</p>
|
<p>Whereof one cannot speak, thereof one must be silent”</p>
|
||||||
<p>– Ludwig Wittgenstein</p>
|
<p>– Ludwig Wittgenstein</p>
|
||||||
@ -75,9 +67,9 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>,
|
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>,
|
||||||
<a
|
<a
|
||||||
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>,
|
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>, <a href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and
|
||||||
<a
|
<a
|
||||||
href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and <a href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
|
href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -21,8 +21,12 @@
|
|||||||
language, should that fail it is possible to manually adjust it. The color
|
language, should that fail it is possible to manually adjust it. The color
|
||||||
scheme for the syntax highlighting is adjustable in settings. </p><pre><code class="language-application-javascript-env-frontend">function helloWorld() {
|
scheme for the syntax highlighting is adjustable in settings. </p><pre><code class="language-application-javascript-env-frontend">function helloWorld() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
alert("Hello world");
|
alert("Hello world");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}</code></pre>
|
}</code></pre>
|
||||||
<p>For larger pieces of code it is better to use a code note, which uses
|
<p>For larger pieces of code it is better to use a code note, which uses
|
||||||
a fully-fledged code editor (CodeMirror). For an example of a code note,
|
a fully-fledged code editor (CodeMirror). For an example of a code note,
|
||||||
|
|||||||
21
apps/edit-docs/demo/root/Trilium Demo/Note Types.html
vendored
Normal file
21
apps/edit-docs/demo/root/Trilium Demo/Note Types.html
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="../../style.css">
|
||||||
|
<base target="_parent">
|
||||||
|
<title data-trilium-title>Note Types</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<h1 data-trilium-h1>Note Types</h1>
|
||||||
|
|
||||||
|
<div class="ck-content">
|
||||||
|
<p>T</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -13,9 +13,8 @@
|
|||||||
<h1 data-trilium-h1>Task manager</h1>
|
<h1 data-trilium-h1>Task manager</h1>
|
||||||
|
|
||||||
<div class="ck-content">
|
<div class="ck-content">
|
||||||
<p>This is a simple TODO/Task manager. You can see some description and explanation
|
<p>This is a simple TODO/Task manager. See the <a href="https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases/task-manager">Trilium documentation</a> for
|
||||||
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a>
|
information on how it works.</p>
|
||||||
</p>
|
|
||||||
<p>Please note that this is meant as scripting example only and feature/bug
|
<p>Please note that this is meant as scripting example only and feature/bug
|
||||||
support is very limited.</p>
|
support is very limited.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,18 +16,32 @@
|
|||||||
<p>Documentation: <a href="http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html">http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html</a>
|
<p>Documentation: <a href="http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html">http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html</a>
|
||||||
</p><pre><code class="language-text-x-sh">#!/bin/bash
|
</p><pre><code class="language-text-x-sh">#!/bin/bash
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# This script opens 4 terminal windows.
|
# This script opens 4 terminal windows.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
i="0"
|
i="0"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
while [ $i -lt 4 ]
|
while [ $i -lt 4 ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
do
|
do
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
xterm &
|
xterm &
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
i=$[$i+1]
|
i=$[$i+1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
done</code></pre>
|
done</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,11 +12,11 @@
|
|||||||
"@triliumnext/desktop": "workspace:*",
|
"@triliumnext/desktop": "workspace:*",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.4.0",
|
"electron": "38.5.0",
|
||||||
"fs-extra": "11.3.2"
|
"fs-extra": "11.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts",
|
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts",
|
||||||
"edit-demo": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
|
"edit-demo": "cross-env TRILIUM_PORT=37744 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js
|
|||||||
import debounce from "@triliumnext/client/src/services/debounce.js";
|
import debounce from "@triliumnext/client/src/services/debounce.js";
|
||||||
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
|
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
|
||||||
import cls from "@triliumnext/server/src/services/cls.js";
|
import cls from "@triliumnext/server/src/services/cls.js";
|
||||||
import type { AdvancedExportOptions } from "@triliumnext/server/src/services/export/zip.js";
|
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/server/src/services/export/zip/abstract_provider.js";
|
||||||
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
|
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
|
||||||
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
|
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||||
|
|
||||||
@ -23,6 +23,8 @@ if (!DOCS_ROOT || !USER_GUIDE_ROOT) {
|
|||||||
throw new Error("Missing DOCS_ROOT or USER_GUIDE_ROOT environment variable.");
|
throw new Error("Missing DOCS_ROOT or USER_GUIDE_ROOT environment variable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BASE_URL = "https://docs.triliumnotes.org";
|
||||||
|
|
||||||
const NOTE_MAPPINGS: NoteMapping[] = [
|
const NOTE_MAPPINGS: NoteMapping[] = [
|
||||||
{
|
{
|
||||||
rootNoteId: "pOsGYCXsbNQG",
|
rootNoteId: "pOsGYCXsbNQG",
|
||||||
@ -75,7 +77,7 @@ async function setOptions() {
|
|||||||
optionsService.setOption("compressImages", "false");
|
optionsService.setOption("compressImages", "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportData(noteId: string, format: "html" | "markdown", outputPath: string, ignoredFiles?: Set<string>) {
|
async function exportData(noteId: string, format: ExportFormat, outputPath: string, ignoredFiles?: Set<string>) {
|
||||||
const zipFilePath = "output.zip";
|
const zipFilePath = "output.zip";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -158,6 +160,14 @@ async function cleanUpMeta(outputPath: string, minify: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
el.isExpanded = false;
|
el.isExpanded = false;
|
||||||
|
|
||||||
|
// Rewrite web view URLs that point to root.
|
||||||
|
if (el.type === "webView" && minify) {
|
||||||
|
const srcAttr = el.attributes.find(attr => attr.name === "webViewSrc");
|
||||||
|
if (srcAttr.value.startsWith("/")) {
|
||||||
|
srcAttr.value = BASE_URL + srcAttr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minify) {
|
if (minify) {
|
||||||
|
|||||||
502
apps/server-e2e/src/exact_search.spec.ts
Normal file
502
apps/server-e2e/src/exact_search.spec.ts
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
const BASE_URL = "http://127.0.0.1:8082";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2E tests for exact search functionality using the leading "=" operator.
|
||||||
|
*
|
||||||
|
* These tests validate the GitHub issue:
|
||||||
|
* - Searching for "pagio" returns many false positives (e.g., "page", "pages")
|
||||||
|
* - Searching for "=pagio" should return ONLY exact matches for "pagio"
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe("Exact Search with Leading = Operator", () => {
|
||||||
|
let csrfToken: string;
|
||||||
|
let createdNoteIds: string[] = [];
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Get CSRF token
|
||||||
|
csrfToken = await page.evaluate(() => {
|
||||||
|
return (window as any).glob.csrfToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(csrfToken).toBeTruthy();
|
||||||
|
|
||||||
|
// Create test notes with specific content patterns
|
||||||
|
// Note 1: Contains exactly "pagio" in title
|
||||||
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with pagio",
|
||||||
|
content: "This note contains the word pagio in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note1.ok()).toBeTruthy();
|
||||||
|
const note1Data = await note1.json();
|
||||||
|
createdNoteIds.push(note1Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 2: Contains "page" (not exact match)
|
||||||
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with page",
|
||||||
|
content: "This note contains the word page in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note2.ok()).toBeTruthy();
|
||||||
|
const note2Data = await note2.json();
|
||||||
|
createdNoteIds.push(note2Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 3: Contains "pages" (plural, not exact match)
|
||||||
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with pages",
|
||||||
|
content: "This note contains the word pages in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note3.ok()).toBeTruthy();
|
||||||
|
const note3Data = await note3.json();
|
||||||
|
createdNoteIds.push(note3Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 4: Contains "homepage" (contains "page", not exact match)
|
||||||
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Homepage Note",
|
||||||
|
content: "This note is about homepage content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note4.ok()).toBeTruthy();
|
||||||
|
const note4Data = await note4.json();
|
||||||
|
createdNoteIds.push(note4Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 5: Another note with exact "pagio" in content
|
||||||
|
const note5 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Another pagio Note",
|
||||||
|
content: "This is another note with pagio content for testing exact matches.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note5.ok()).toBeTruthy();
|
||||||
|
const note5Data = await note5.json();
|
||||||
|
createdNoteIds.push(note5Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 6: Contains "pagio" in title only
|
||||||
|
const note6 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "pagio",
|
||||||
|
content: "This note has pagio as the title.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note6.ok()).toBeTruthy();
|
||||||
|
const note6Data = await note6.json();
|
||||||
|
createdNoteIds.push(note6Data.note.noteId);
|
||||||
|
|
||||||
|
// Wait a bit for indexing
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }) => {
|
||||||
|
// Clean up created notes
|
||||||
|
for (const noteId of createdNoteIds) {
|
||||||
|
try {
|
||||||
|
const taskId = `cleanup-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
await page.request.delete(`${BASE_URL}/api/notes/${noteId}?taskId=${taskId}&last=true`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to delete note ${noteId}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createdNoteIds = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Quick search without = operator returns all partial matches", async ({ page }) => {
|
||||||
|
// Test the /quick-search endpoint without the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/pag`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return multiple notes including "page", "pages", "homepage"
|
||||||
|
expect(data.searchResultNoteIds).toBeDefined();
|
||||||
|
expect(data.searchResults).toBeDefined();
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testResults = data.searchResults.filter((result: any) =>
|
||||||
|
result.noteTitle.includes("page") ||
|
||||||
|
result.noteTitle.includes("pagio") ||
|
||||||
|
result.noteTitle.includes("Homepage")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should find at least "page", "pages", "homepage", and "pagio" notes
|
||||||
|
expect(testResults.length).toBeGreaterThanOrEqual(4);
|
||||||
|
|
||||||
|
console.log("Quick search 'pag' found:", testResults.length, "matching notes");
|
||||||
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Quick search with = operator returns only exact matches", async ({ page }) => {
|
||||||
|
// Test the /quick-search endpoint WITH the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=pagio`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return only notes with exact "pagio" match
|
||||||
|
expect(data.searchResultNoteIds).toBeDefined();
|
||||||
|
expect(data.searchResults).toBeDefined();
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testResults = data.searchResults.filter((result: any) =>
|
||||||
|
createdNoteIds.includes(result.notePath.split("/").pop() || "")
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Quick search '=pagio' found:", testResults.length, "matching notes");
|
||||||
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find exactly 3 notes: "Test Note with pagio", "Another pagio Note", "pagio"
|
||||||
|
expect(testResults.length).toBe(3);
|
||||||
|
|
||||||
|
// Verify that none of the results contain "page" or "pages" (only "pagio")
|
||||||
|
for (const result of testResults) {
|
||||||
|
const title = result.noteTitle.toLowerCase();
|
||||||
|
const hasPageNotPagio = (title.includes("page") && !title.includes("pagio"));
|
||||||
|
expect(hasPageNotPagio).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Full search API without = operator returns partial matches", async ({ page }) => {
|
||||||
|
// Test the /search endpoint without the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/search/pag`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return an array of note IDs
|
||||||
|
expect(Array.isArray(data)).toBe(true);
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
||||||
|
|
||||||
|
console.log("Full search 'pag' found:", testNoteIds.length, "matching notes from our test set");
|
||||||
|
|
||||||
|
// Should find at least 4 notes
|
||||||
|
expect(testNoteIds.length).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Full search API with = operator returns only exact matches", async ({ page }) => {
|
||||||
|
// Test the /search endpoint WITH the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/search/=pagio`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return an array of note IDs
|
||||||
|
expect(Array.isArray(data)).toBe(true);
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
||||||
|
|
||||||
|
console.log("Full search '=pagio' found:", testNoteIds.length, "matching notes from our test set");
|
||||||
|
|
||||||
|
// Should find exactly 3 notes with exact "pagio" match
|
||||||
|
expect(testNoteIds.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact search operator works with content search", async ({ page }) => {
|
||||||
|
// Create a note with "test" in title but different content
|
||||||
|
const noteWithTest = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing Content",
|
||||||
|
content: "This note contains the exact word test in content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteWithTest.ok()).toBeTruthy();
|
||||||
|
const noteWithTestData = await noteWithTest.json();
|
||||||
|
const testNoteId = noteWithTestData.note.noteId;
|
||||||
|
createdNoteIds.push(testNoteId);
|
||||||
|
|
||||||
|
// Create a note with "testing" (not exact match)
|
||||||
|
const noteWithTesting = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing More",
|
||||||
|
content: "This note has testing in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteWithTesting.ok()).toBeTruthy();
|
||||||
|
const noteWithTestingData = await noteWithTesting.json();
|
||||||
|
createdNoteIds.push(noteWithTestingData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search with exact operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=test`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === testNoteId || noteId === noteWithTestingData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact search '=test' found our test notes:", ourTestNotes.length);
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find the note with exact "test" match, but not "testing"
|
||||||
|
// Note: This test may fail if the implementation doesn't properly handle exact matching in content
|
||||||
|
expect(ourTestNotes.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact search is case-insensitive", async ({ page }) => {
|
||||||
|
// Create notes with different case variations
|
||||||
|
const noteUpper = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "EXACT MATCH",
|
||||||
|
content: "This note has EXACT in uppercase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteUpper.ok()).toBeTruthy();
|
||||||
|
const noteUpperData = await noteUpper.json();
|
||||||
|
createdNoteIds.push(noteUpperData.note.noteId);
|
||||||
|
|
||||||
|
const noteLower = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact match",
|
||||||
|
content: "This note has exact in lowercase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteLower.ok()).toBeTruthy();
|
||||||
|
const noteLowerData = await noteLower.json();
|
||||||
|
createdNoteIds.push(noteLowerData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search with exact operator in lowercase
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=exact`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === noteUpperData.note.noteId || noteId === noteLowerData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Case-insensitive exact search found:", ourTestNotes.length, "notes");
|
||||||
|
|
||||||
|
// Should find both uppercase and lowercase versions
|
||||||
|
expect(ourTestNotes.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact phrase matching with multi-word searches", async ({ page }) => {
|
||||||
|
// Create notes with various phrase patterns
|
||||||
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact phrase",
|
||||||
|
content: "This note contains the exact phrase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note1.ok()).toBeTruthy();
|
||||||
|
const note1Data = await note1.json();
|
||||||
|
createdNoteIds.push(note1Data.note.noteId);
|
||||||
|
|
||||||
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact phrase match",
|
||||||
|
content: "This note has exact phrase followed by more words.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note2.ok()).toBeTruthy();
|
||||||
|
const note2Data = await note2.json();
|
||||||
|
createdNoteIds.push(note2Data.note.noteId);
|
||||||
|
|
||||||
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "phrase exact",
|
||||||
|
content: "This note has the words in reverse order.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note3.ok()).toBeTruthy();
|
||||||
|
const note3Data = await note3.json();
|
||||||
|
createdNoteIds.push(note3Data.note.noteId);
|
||||||
|
|
||||||
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "this exact and that phrase",
|
||||||
|
content: "Words are separated but both present.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note4.ok()).toBeTruthy();
|
||||||
|
const note4Data = await note4.json();
|
||||||
|
createdNoteIds.push(note4Data.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for exact phrase "exact phrase"
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='exact phrase'`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return [note1Data.note.noteId, note2Data.note.noteId, note3Data.note.noteId, note4Data.note.noteId].includes(noteId || "");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact phrase search '=\"exact phrase\"' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only notes 1 and 2 (consecutive "exact phrase")
|
||||||
|
// Should NOT find note 3 (reversed order) or note 4 (words separated)
|
||||||
|
expect(ourTestNotes.length).toBe(2);
|
||||||
|
|
||||||
|
const foundTitles = ourTestNotes.map((r: any) => r.noteTitle);
|
||||||
|
expect(foundTitles).toContain("exact phrase");
|
||||||
|
expect(foundTitles).toContain("exact phrase match");
|
||||||
|
expect(foundTitles).not.toContain("phrase exact");
|
||||||
|
expect(foundTitles).not.toContain("this exact and that phrase");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact phrase matching respects word order", async ({ page }) => {
|
||||||
|
// Create notes to test word order sensitivity
|
||||||
|
const noteForward = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing Order",
|
||||||
|
content: "This is a test sentence for verification.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteForward.ok()).toBeTruthy();
|
||||||
|
const noteForwardData = await noteForward.json();
|
||||||
|
createdNoteIds.push(noteForwardData.note.noteId);
|
||||||
|
|
||||||
|
const noteReverse = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Order Testing",
|
||||||
|
content: "A sentence test is this for verification.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteReverse.ok()).toBeTruthy();
|
||||||
|
const noteReverseData = await noteReverse.json();
|
||||||
|
createdNoteIds.push(noteReverseData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for exact phrase "test sentence"
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='test sentence'`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === noteForwardData.note.noteId || noteId === noteReverseData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact phrase search '=\"test sentence\"' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only the forward order note
|
||||||
|
expect(ourTestNotes.length).toBe(1);
|
||||||
|
expect(ourTestNotes[0].noteTitle).toBe("Testing Order");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multi-word exact search without quotes", async ({ page }) => {
|
||||||
|
// Test that multi-word search with = but without quotes also does exact phrase matching
|
||||||
|
const notePhrase = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Quick Test Note",
|
||||||
|
content: "A simple note for multi word testing.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(notePhrase.ok()).toBeTruthy();
|
||||||
|
const notePhraseData = await notePhrase.json();
|
||||||
|
createdNoteIds.push(notePhraseData.note.noteId);
|
||||||
|
|
||||||
|
const noteScattered = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Word Multi Testing",
|
||||||
|
content: "Words are multi scattered in this testing example.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteScattered.ok()).toBeTruthy();
|
||||||
|
const noteScatteredData = await noteScattered.json();
|
||||||
|
createdNoteIds.push(noteScatteredData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for "=multi word" without quotes (parser tokenizes as two words)
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=multi word`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === notePhraseData.note.noteId || noteId === noteScatteredData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Multi-word exact search '=multi word' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only the note with consecutive "multi word" phrase
|
||||||
|
expect(ourTestNotes.length).toBe(1);
|
||||||
|
expect(ourTestNotes[0].noteTitle).toBe("Quick Test Note");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:22.21.0-bullseye-slim AS builder
|
FROM node:24.11.0-bullseye-slim AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:22.21.0-bullseye-slim
|
FROM node:24.11.0-bullseye-slim
|
||||||
# Install only runtime dependencies
|
# Install only runtime dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:22.21.0-alpine AS builder
|
FROM node:24.11.0-alpine AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:22.21.0-alpine
|
FROM node:24.11.0-alpine
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
RUN apk add --no-cache su-exec shadow
|
RUN apk add --no-cache su-exec shadow
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:22.21.0-alpine AS builder
|
FROM node:24.11.0-alpine AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:22.21.0-alpine
|
FROM node:24.11.0-alpine
|
||||||
# Create a non-root user with configurable UID/GID
|
# Create a non-root user with configurable UID/GID
|
||||||
ARG USER=trilium
|
ARG USER=trilium
|
||||||
ARG UID=1001
|
ARG UID=1001
|
||||||
|
|||||||
28
apps/server/Dockerfile.legacy
Normal file
28
apps/server/Dockerfile.legacy
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
FROM node:22.21.0-bullseye-slim AS builder
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
# Install native dependencies since we might be building cross-platform.
|
||||||
|
WORKDIR /usr/src/app/build
|
||||||
|
COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||||
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
|
FROM node:22.21.0-bullseye-slim
|
||||||
|
# Install only runtime dependencies
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
gosu && \
|
||||||
|
rm -rf \
|
||||||
|
/var/lib/apt/lists/* \
|
||||||
|
/var/cache/apt/*
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY ./dist /usr/src/app
|
||||||
|
RUN rm -rf /usr/src/app/node_modules/better-sqlite3
|
||||||
|
COPY --from=builder /usr/src/app/node_modules/better-sqlite3 /usr/src/app/node_modules/better-sqlite3
|
||||||
|
COPY ./start-docker.sh /usr/src/app
|
||||||
|
|
||||||
|
# Configure container
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD [ "sh", "./start-docker.sh" ]
|
||||||
|
HEALTHCHECK --start-period=10s CMD exec gosu node node /usr/src/app/docker_healthcheck.cjs
|
||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:22.21.0-bullseye-slim AS builder
|
FROM node:24.11.0-bullseye-slim AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:22.21.0-bullseye-slim
|
FROM node:24.11.0-bullseye-slim
|
||||||
# Create a non-root user with configurable UID/GID
|
# Create a non-root user with configurable UID/GID
|
||||||
ARG USER=trilium
|
ARG USER=trilium
|
||||||
ARG UID=1001
|
ARG UID=1001
|
||||||
|
|||||||
@ -4,12 +4,12 @@ info:
|
|||||||
title: ETAPI
|
title: ETAPI
|
||||||
description: External Trilium API
|
description: External Trilium API
|
||||||
contact:
|
contact:
|
||||||
name: zadam
|
name: Trilium Notes Team
|
||||||
email: zadam.apps@gmail.com
|
email: contact@eliandoran.me
|
||||||
url: https://github.com/zadam/trilium
|
url: https://triliumnotes.org
|
||||||
license:
|
license:
|
||||||
name: Apache 2.0
|
name: GNU Affero General Public License v3.0 only
|
||||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
url: https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||||
servers:
|
servers:
|
||||||
- url: http://localhost:37740/etapi
|
- url: http://localhost:37740/etapi
|
||||||
- url: http://localhost:8080/etapi
|
- url: http://localhost:8080/etapi
|
||||||
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: Trilium Notes Internal API
|
title: Internal Trilium API
|
||||||
version: 0.98.0
|
version: 0.99.3
|
||||||
description: |
|
description: |
|
||||||
This is the internal API used by the Trilium Notes client application.
|
This is the internal API used by the Trilium Notes client application.
|
||||||
|
|
||||||
@ -24,11 +24,12 @@ info:
|
|||||||
State-changing operations require CSRF tokens when using session authentication.
|
State-changing operations require CSRF tokens when using session authentication.
|
||||||
|
|
||||||
contact:
|
contact:
|
||||||
name: TriliumNext Issue Tracker
|
name: Trilium Notes Team
|
||||||
url: https://github.com/TriliumNext/Trilium/issues
|
email: contact@eliandoran.me
|
||||||
|
url: https://triliumnotes.org
|
||||||
license:
|
license:
|
||||||
name: GNU Affero General Public License v3.0
|
name: GNU Affero General Public License v3.0 only
|
||||||
url: https://www.gnu.org/licenses/agpl-3.0.html
|
url: https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||||
|
|
||||||
servers:
|
servers:
|
||||||
- url: http://localhost:8080
|
- url: http://localhost:8080
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user