mirror of
https://github.com/zadam/trilium.git
synced 2025-11-12 16:25:51 +01:00
Compare commits
287 Commits
feat/rice-
...
v0.99.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25563c6687 | ||
|
|
48608adbd3 | ||
|
|
362ecba98d | ||
|
|
a13892da66 | ||
|
|
e8dc19a1a6 | ||
|
|
cc326547d1 | ||
|
|
a45b147462 | ||
|
|
4e73f20165 | ||
|
|
80c77eeb18 | ||
|
|
c30282fbd0 | ||
|
|
7b8f1ed6ec | ||
|
|
118e11c3fd | ||
|
|
b54765113e | ||
|
|
ed08893996 | ||
|
|
20286d53c8 | ||
|
|
9c1a34fe7c | ||
|
|
e70c6b69b8 | ||
|
|
9b69b0ad0d | ||
|
|
3776c40b8d | ||
|
|
c7369bc9b3 | ||
|
|
b741662fde | ||
|
|
624610b17c | ||
|
|
de004bd8ba | ||
|
|
06eb30c69d | ||
|
|
b9b5c13d9c | ||
|
|
8e697d0578 | ||
|
|
6f245ec8d5 | ||
|
|
532df6559a | ||
|
|
8589f7f164 | ||
|
|
4eadf40e20 | ||
|
|
7b9303b392 | ||
|
|
521152ec0e | ||
|
|
2ff746253d | ||
|
|
c270aef738 | ||
|
|
d0881c09ed | ||
|
|
b905c1d03a | ||
|
|
f03448bae4 | ||
|
|
f8ac09df38 | ||
|
|
15c329c331 | ||
|
|
f6afc0b718 | ||
|
|
ecfa333491 | ||
|
|
21b0ef9554 | ||
|
|
2f3be96dff | ||
|
|
4ba7907bee | ||
|
|
afa92551ea | ||
|
|
cc0646e79c | ||
|
|
4c8f20be9a | ||
|
|
3407528c03 | ||
|
|
70575a00cb | ||
|
|
74ba4b9ee5 | ||
|
|
706abeb307 | ||
|
|
7158c48831 | ||
|
|
78e2814068 | ||
|
|
7659224e3a | ||
|
|
baff349fa2 | ||
|
|
50869d29db | ||
|
|
c4603fce25 | ||
|
|
220aab2b76 | ||
|
|
285a7253e3 | ||
|
|
d8d80ed936 | ||
|
|
3463cb83a0 | ||
|
|
b0bd60b9a4 | ||
|
|
3d70a0534b | ||
|
|
53805e9c49 | ||
|
|
0e95610d4e | ||
|
|
051e2b4eef | ||
|
|
1d750bde64 | ||
|
|
4476615d33 | ||
|
|
bbcc16daab | ||
|
|
457dd070c6 | ||
|
|
ce229dd6f5 | ||
|
|
2e59d9d7bc | ||
|
|
bbe96c3967 | ||
|
|
d95eb9f5d3 | ||
|
|
d75279316a | ||
|
|
7f6be13a18 | ||
|
|
2f74b40095 | ||
|
|
a844e1faab | ||
|
|
f629f564cd | ||
|
|
9a5f2f8d3b | ||
|
|
9bccc72668 | ||
|
|
a29597a4bf | ||
|
|
44b34d1ea0 | ||
|
|
c617c84d86 | ||
|
|
b38780755a | ||
|
|
78a54fa9f7 | ||
|
|
fa64ca2c93 | ||
|
|
71d7403690 | ||
|
|
e4cd946ea8 | ||
|
|
cedd1c4789 | ||
|
|
7a677cff5f | ||
|
|
2d03dd22e3 | ||
|
|
e28da416ba | ||
|
|
28c0ef52f3 | ||
|
|
9464a64d64 | ||
|
|
c3af6a6aa2 | ||
|
|
a4281fe26f | ||
|
|
5c0d6e1fef | ||
|
|
23f1103822 | ||
|
|
b45ee6879c | ||
|
|
a8116aa264 | ||
|
|
db3960a23e | ||
|
|
a43e08500e | ||
|
|
9011d648b5 | ||
|
|
30c1708979 | ||
|
|
1a55d3433d | ||
|
|
bec47c0bb2 | ||
|
|
4fdb502a19 | ||
|
|
e9ccd52fd5 | ||
|
|
dcd30972bd | ||
|
|
4578541fa8 | ||
|
|
914fa3625f | ||
|
|
5e3ffc12ce | ||
|
|
aa3a8d19ae | ||
|
|
a57c237c69 | ||
|
|
b52e615f0c | ||
|
|
95e5c2563e | ||
|
|
33be7f828b | ||
|
|
d23d37baac | ||
|
|
dda8b2795b | ||
|
|
0b9eb6c532 | ||
|
|
728f574eac | ||
|
|
8e90826aef | ||
|
|
5e5e0afcf0 | ||
|
|
de85d1f0df | ||
|
|
b287b892e1 | ||
|
|
a0edf00caa | ||
|
|
218b9404fc | ||
|
|
54af120e96 | ||
|
|
ba61ab18ff | ||
|
|
2e14522f86 | ||
|
|
4ae38ac5d6 | ||
|
|
d5cb6a86c8 | ||
|
|
a577fd45e2 | ||
|
|
977284fe57 | ||
|
|
26ea43d604 | ||
|
|
4cb328bdb3 | ||
|
|
16785a5c0b | ||
|
|
d271fe7fdd | ||
|
|
88b9709f15 | ||
|
|
f55edabe92 | ||
|
|
2b983f871e | ||
|
|
ab298cbb3b | ||
|
|
abeeea584f | ||
|
|
4d5597cc75 | ||
|
|
c684712141 | ||
|
|
a8bb301296 | ||
|
|
d5bfa466a2 | ||
|
|
7651c58c47 | ||
|
|
c2ce36d963 | ||
|
|
3359ff5470 | ||
|
|
421c1f257e | ||
|
|
97e87741ee | ||
|
|
193caf8c21 | ||
|
|
d521bda6ab | ||
|
|
b80cb22985 | ||
|
|
7131d44d03 | ||
|
|
7369f9d532 | ||
|
|
58ac325634 | ||
|
|
579b2ce76e | ||
|
|
0494032fb5 | ||
|
|
48853555f5 | ||
|
|
0cd6f0d267 | ||
|
|
0ae4defc6d | ||
|
|
db644f20ed | ||
|
|
59a2ef7527 | ||
|
|
757a046474 | ||
|
|
aeb0f44a43 | ||
|
|
5186ea3fff | ||
|
|
70a4feff50 | ||
|
|
91f85e6675 | ||
|
|
0cb989e74f | ||
|
|
d4e31e9d98 | ||
|
|
6c3b5314c8 | ||
|
|
3faac9f26e | ||
|
|
c31ac1a6ee | ||
|
|
9eff6ad4c2 | ||
|
|
9deb7ba4e9 | ||
|
|
93d77ca06e | ||
|
|
a42daccc2e | ||
|
|
33c64b604e | ||
|
|
f89c14b35a | ||
|
|
1fa3420abe | ||
|
|
f8b8edd5aa | ||
|
|
4c90319c9e | ||
|
|
3b531544a3 | ||
|
|
25d9695db0 | ||
|
|
caf88473f6 | ||
|
|
b32dc18cf6 | ||
|
|
5a7349121a | ||
|
|
dcc5b9f422 | ||
|
|
0b01890a11 | ||
|
|
686c8936cb | ||
|
|
d9071f2d8e | ||
|
|
2a0472ae42 | ||
|
|
8c2354df71 | ||
|
|
d650b801e6 | ||
|
|
fbb27b512e | ||
|
|
3ae7bd59ec | ||
|
|
55f7a26634 | ||
|
|
aa7f01313a | ||
|
|
6f82c283e9 | ||
|
|
aa7ecaf684 | ||
|
|
50a69248a7 | ||
|
|
5782e58db1 | ||
|
|
e8bf12c4ab | ||
|
|
344f2d819e | ||
|
|
d2e9101675 | ||
|
|
82e5de2261 | ||
|
|
ff4cd7eae5 | ||
|
|
a62c9a1a2f | ||
|
|
cf406383c3 | ||
|
|
07fe42d04e | ||
|
|
5b8bb8587d | ||
|
|
154492e454 | ||
|
|
b1729ad7ec | ||
|
|
daec11b981 | ||
|
|
7cdd8ffbe2 | ||
|
|
25ac9e2aa1 | ||
|
|
4fc434a52e | ||
|
|
3e0d1bfa44 | ||
|
|
3b02eb8851 | ||
|
|
4c5b2a7c75 | ||
|
|
e6d2009605 | ||
|
|
5d706a88d8 | ||
|
|
c7c7e05106 | ||
|
|
e250107202 | ||
|
|
c141cbcf07 | ||
|
|
8e83562e6a | ||
|
|
08197f56d0 | ||
|
|
ff0d8a70ad | ||
|
|
605b317f29 | ||
|
|
6f0a264869 | ||
|
|
6a89e096e5 | ||
|
|
e4e0f7619b | ||
|
|
f3f07cdd28 | ||
|
|
ef82c3d48b | ||
|
|
214ba5265a | ||
|
|
ecb6dc7923 | ||
|
|
131fb43ab7 | ||
|
|
8a76fdb8d1 | ||
|
|
8e3dbb2f65 | ||
|
|
8b4fee1680 | ||
|
|
a370b52614 | ||
|
|
4063229982 | ||
|
|
1ef03b7a77 | ||
|
|
e510653edb | ||
|
|
a2c523def1 | ||
|
|
e3604edad7 | ||
|
|
426d8296be | ||
|
|
947e43a615 | ||
|
|
0424fe4fba | ||
|
|
f789b69506 | ||
|
|
5df512a69c | ||
|
|
2a5f329ada | ||
|
|
4fe3944585 | ||
|
|
98b8e97fd9 | ||
|
|
38a1cd0d35 | ||
|
|
ae544a80c2 | ||
|
|
8d88411fda | ||
|
|
ea45024559 | ||
|
|
64d3589b40 | ||
|
|
638cb4281e | ||
|
|
1568908982 | ||
|
|
4459561308 | ||
|
|
3341e59a80 | ||
|
|
74a805056b | ||
|
|
f42e870de1 | ||
|
|
ca3964f8b7 | ||
|
|
ddafda5f4e | ||
|
|
40b08e1828 | ||
|
|
5141f0a0d5 | ||
|
|
8c165c0401 | ||
|
|
b4dd40e128 | ||
|
|
535b960b76 | ||
|
|
b58f37cd4a | ||
|
|
a01fb39599 | ||
|
|
be15934b22 | ||
|
|
96b3464f00 | ||
|
|
2470b0b334 | ||
|
|
4344687303 | ||
|
|
b4fe46eba3 | ||
|
|
8e227a6146 | ||
|
|
b03cb1ce1b | ||
|
|
fb0d971e48 | ||
|
|
4fa4112840 | ||
|
|
50f0b88eff |
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
|
||||
54
.github/workflows/playwright.yml
vendored
54
.github/workflows/playwright.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- hotfix
|
||||
paths-ignore:
|
||||
- "apps/website/**"
|
||||
pull_request:
|
||||
@@ -13,8 +14,24 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
e2e:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: linux-x64
|
||||
os: ubuntu-22.04
|
||||
arch: x64
|
||||
- name: linux-arm64
|
||||
os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: E2E tests on ${{ matrix.name }}
|
||||
env:
|
||||
TRILIUM_DOCKER: 1
|
||||
TRILIUM_PORT: 8082
|
||||
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
||||
TRILIUM_INTEGRATION_TEST: memory
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -29,9 +46,34 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: pnpm exec playwright install --with-deps
|
||||
|
||||
- run: pnpm --filter server-e2e e2e
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Build the server
|
||||
uses: ./.github/actions/build-server
|
||||
with:
|
||||
os: linux
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Unpack and start the server
|
||||
run: |
|
||||
version=$(node --eval "console.log(require('./package.json').version)")
|
||||
file=$(find ./upload -name '*.tar.xz' -print -quit)
|
||||
name=$(basename "$file" .tar.xz)
|
||||
mkdir -p ./server-dist
|
||||
tar -xvf "$file" -C ./server-dist
|
||||
server_dir="./server-dist/TriliumNotes-Server-$version-linux-${{ matrix.arch }}"
|
||||
if [ ! -d "$server_dir" ]; then
|
||||
echo Missing dir.
|
||||
exit 1
|
||||
fi
|
||||
cd "$server_dir"
|
||||
"./trilium.sh" &
|
||||
sleep 10
|
||||
|
||||
- name: Server end-to-end tests
|
||||
run: pnpm --filter server-e2e e2e
|
||||
|
||||
- name: Upload test report
|
||||
if: failure()
|
||||
@@ -39,3 +81,7 @@ jobs:
|
||||
with:
|
||||
name: e2e report
|
||||
path: apps/server-e2e/test-output
|
||||
|
||||
- name: Kill the server
|
||||
if: always()
|
||||
run: pkill -f trilium || true
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -127,7 +127,7 @@ jobs:
|
||||
path: upload
|
||||
|
||||
- name: Publish stable release
|
||||
uses: softprops/action-gh-release@v2.4.1
|
||||
uses: softprops/action-gh-release@v2.4.2
|
||||
with:
|
||||
draft: false
|
||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||
|
||||
@@ -41,12 +41,12 @@
|
||||
"@types/node": "24.10.0",
|
||||
"@types/yargs": "17.0.34",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.39.0",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.5",
|
||||
"lorem-ipsum": "2.0.8",
|
||||
"rcedit": "4.0.1",
|
||||
"rcedit": "5.0.0",
|
||||
"rimraf": "6.1.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.20.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.10.0",
|
||||
"@redocly/cli": "2.11.0",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.2",
|
||||
"react": "19.2.0",
|
||||
|
||||
@@ -22,8 +22,9 @@ async function main() {
|
||||
buildSwagger(context);
|
||||
buildScriptApi(context);
|
||||
|
||||
// Copy index file.
|
||||
// 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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.99.3",
|
||||
"version": "0.99.5",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -15,7 +15,7 @@
|
||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.39.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
@@ -43,7 +43,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "16.5.0",
|
||||
"i18next": "25.6.0",
|
||||
"i18next": "25.6.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -53,13 +53,13 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "16.4.1",
|
||||
"marked": "16.4.2",
|
||||
"mermaid": "11.12.1",
|
||||
"mind-elixir": "5.3.4",
|
||||
"mind-elixir": "5.3.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.2",
|
||||
"react-i18next": "16.2.3",
|
||||
"react-i18next": "16.2.4",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
|
||||
@@ -499,6 +499,10 @@ type EventMappings = {
|
||||
noteIds: string[];
|
||||
};
|
||||
refreshData: { ntxId: string | null | undefined };
|
||||
contentSafeMarginChanged: {
|
||||
top: number;
|
||||
noteContext: NoteContext;
|
||||
}
|
||||
};
|
||||
|
||||
export type EventListener<T extends EventNames> = {
|
||||
|
||||
@@ -7,7 +7,6 @@ import protectedSessionService from "../services/protected_session.js";
|
||||
import options from "../services/options.js";
|
||||
import froca from "../services/froca.js";
|
||||
import utils from "../services/utils.js";
|
||||
import LlmChatPanel from "../widgets/llm_chat_panel.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
|
||||
@@ -171,7 +170,8 @@ export default class RootCommandExecutor extends Component {
|
||||
}
|
||||
|
||||
toggleTrayCommand() {
|
||||
if (!utils.isElectron()) return;
|
||||
if (!utils.isElectron() || options.is("disableTray")) return;
|
||||
|
||||
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
|
||||
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
|
||||
const isVisible = windows.every((w) => w.isVisible());
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||
import SpacerWidget from "../widgets/spacer.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import options from "../services/options.js";
|
||||
import utils from "../services/utils.js";
|
||||
import type { AppContext } from "../components/app_context.js";
|
||||
import type { WidgetsByParent } from "../services/bundle.js";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import ApiLog from "../widgets/api_log.jsx";
|
||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||
import ContentHeader from "../widgets/containers/content-header.js";
|
||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import options from "../services/options.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
import SpacerWidget from "../widgets/spacer.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import SqlResults from "../widgets/sql_result.js";
|
||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||
import ApiLog from "../widgets/api_log.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
import type { AppContext } from "../components/app_context.js";
|
||||
import type { WidgetsByParent } from "../services/bundle.js";
|
||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import utils from "../services/utils.js";
|
||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@@ -129,12 +131,15 @@ export default class DesktopLayout {
|
||||
.child(<CreatePaneButton />)
|
||||
)
|
||||
.child(<Ribbon />)
|
||||
.child(<SharedInfo />)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.child(new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfo />)
|
||||
)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(new NoteDetailWidget())
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import ContentHeader from "../widgets/containers/content-header.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import type AppContext from "../components/app_context.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -149,13 +151,16 @@ export default class MobileLayout {
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<SharedInfoWidget />)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfoWidget />)
|
||||
)
|
||||
.child(new NoteDetailWidget())
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
command: "editBranchPrefix",
|
||||
keyboardShortcut: "editBranchPrefix",
|
||||
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 },
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "boxicons/css/boxicons.min.css";
|
||||
|
||||
:root {
|
||||
--print-font-size: 11pt;
|
||||
--ck-content-color-image-caption-background: transparent !important;
|
||||
|
||||
@@ -70,6 +70,9 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Check custom CSS.
|
||||
await loadCustomCss(note);
|
||||
}
|
||||
|
||||
load().then(() => requestAnimationFrame(onReady))
|
||||
@@ -89,7 +92,10 @@ function CollectionRenderer({ note, onReady }: RendererProps) {
|
||||
ntxId="print"
|
||||
highlightedTokens={null}
|
||||
media="print"
|
||||
onReady={onReady}
|
||||
onReady={async () => {
|
||||
await loadCustomCss(note);
|
||||
onReady();
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -102,4 +108,25 @@ function Error404({ noteId }: { noteId: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
async function loadCustomCss(note: FNote) {
|
||||
const printCssNotes = await note.getRelationTargets("printCss");
|
||||
let loadPromises: JQueryPromise<void>[] = [];
|
||||
|
||||
for (const printCssNote of printCssNotes) {
|
||||
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
|
||||
|
||||
const linkEl = document.createElement("link");
|
||||
linkEl.href = `/api/notes/${printCssNote.noteId}/download`;
|
||||
linkEl.rel = "stylesheet";
|
||||
|
||||
const promise = $.Deferred();
|
||||
loadPromises.push(promise.promise());
|
||||
linkEl.onload = () => promise.resolve();
|
||||
|
||||
document.head.appendChild(linkEl);
|
||||
}
|
||||
|
||||
await Promise.allSettled(loadPromises);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("shortcuts", () => {
|
||||
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
||||
|
||||
// Special keys
|
||||
for (const keyCode of [ "Delete", "Enter" ]) {
|
||||
for (const keyCode of [ "Delete", "Enter", "NumpadEnter" ]) {
|
||||
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
||||
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([
|
||||
"Delete",
|
||||
"Enter",
|
||||
"NumpadEnter",
|
||||
...functionKeyCodes
|
||||
]);
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob.device !== "print") {
|
||||
applyCopyToClipboardButton($(codeBlock));
|
||||
}
|
||||
|
||||
if (syntaxHighlightingEnabled) {
|
||||
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
||||
|
||||
@@ -841,7 +841,7 @@ export function arrayEqual<T>(a: T[], b: T[]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
type Indexed<T extends object> = T & { index: number };
|
||||
export type Indexed<T extends object> = T & { index: number };
|
||||
|
||||
/**
|
||||
* Given an object array, alters every object in the array to have an index field assigned to it.
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
.note-detail-relation-map {
|
||||
height: 100%;
|
||||
overflow: hidden !important;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -1104,7 +1104,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
|
||||
.card {
|
||||
color: inherit !important;
|
||||
background-color: inherit !important;
|
||||
border-color: var(--main-border-color) !important;
|
||||
}
|
||||
|
||||
@@ -1759,10 +1758,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
flex-direction: column;
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: 5px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#right-pane .card-header {
|
||||
background: inherit;
|
||||
padding: 6px 0 3px 0;
|
||||
width: 99%; /* to give minimal right margin */
|
||||
background-color: var(--button-background-color);
|
||||
@@ -1809,12 +1808,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
}
|
||||
|
||||
.note-split {
|
||||
/* Limits the maximum width of the note */
|
||||
--max-content-width: var(--preferred-max-content-width);
|
||||
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
.note-split.full-content-width {
|
||||
max-width: 999999px;
|
||||
--max-content-width: unset;
|
||||
}
|
||||
|
||||
button.close:hover {
|
||||
@@ -2034,13 +2036,16 @@ body.zen #right-pane,
|
||||
body.zen #mobile-sidebar-wrapper,
|
||||
body.zen .tab-row-container,
|
||||
body.zen .tab-row-widget,
|
||||
body.zen .shared-info-widget,
|
||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
|
||||
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
|
||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
|
||||
body.zen .note-icon-widget,
|
||||
body.zen .title-row .icon-action,
|
||||
body.zen .promoted-attributes-widget,
|
||||
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
|
||||
body.zen .action-button {
|
||||
body.zen .action-button,
|
||||
body.zen .note-split:not(.type-book) .note-list-widget {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -2084,12 +2089,121 @@ body.zen .note-title-widget,
|
||||
body.zen .note-title-widget input {
|
||||
font-size: 1rem !important;
|
||||
background: transparent !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.zen #detail-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.zen .note-split:not(.full-content-width) .scrolling-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scroll-behavior: unset !important;
|
||||
}
|
||||
|
||||
body.zen .note-split:not(.full-content-width) .note-detail {
|
||||
margin: auto;
|
||||
padding-bottom: 25vh;
|
||||
max-width: var(--max-content-width);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.zen .note-split:not(.full-content-width) .scroll-padding-widget {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.zen .note-split.type-text {
|
||||
position: relative;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .title-row {
|
||||
--start-color: var(--main-background-color);
|
||||
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: linear-gradient(var(--start-color) 30%, transparent 100%);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@supports (background: color-mix(in srgb, white, transparent)) {
|
||||
body.zen .note-split.type-text .title-row {
|
||||
--start-color: color-mix(in srgb, var(--main-background-color), transparent 10%);
|
||||
}
|
||||
}
|
||||
|
||||
body.zen .note-split.type-text .scrolling-container {
|
||||
--padding-bottom: 130px; /* Should be enough to avoid caret being hidden by the formatting toolbar */
|
||||
|
||||
/* (Usually) keeps the caret above the fixed toolbar */
|
||||
scroll-padding-bottom: var(--padding-bottom);
|
||||
}
|
||||
|
||||
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
|
||||
--padding-top: 50px; /* Should be enough to cover the title row */
|
||||
|
||||
padding-top: var(--padding-top);
|
||||
scroll-padding-top: var(--padding-top);
|
||||
}
|
||||
|
||||
/* Fixed formatting toolbar */
|
||||
|
||||
body.zen .note-split .ribbon-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 20px;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
opacity: 0; /* Hidden unless the current note split is focused */
|
||||
pointer-events: none;
|
||||
transition: opacity 100ms linear;
|
||||
}
|
||||
|
||||
body.zen .note-split:focus-within .ribbon-container {
|
||||
opacity: 1; /* Show when the note split is focused */
|
||||
}
|
||||
|
||||
body.zen .note-split .ribbon-container .ribbon-body {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
padding: 4px;
|
||||
background: var(--menu-background-color);
|
||||
}
|
||||
|
||||
body.zen .note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
|
||||
/* Hide the toolbar wrapper if the toolbar is missing */
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.zen .note-split:focus-within .ribbon-container .classic-toolbar-widget {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@media (max-width: 1300px) {
|
||||
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
||||
/* Set the toolbar to full with */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
|
||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
|
||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
|
||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
|
||||
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
|
||||
/* Force toolbar items overflow dropdowns open upwards */
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Content renderer */
|
||||
|
||||
footer.file-footer,
|
||||
@@ -2406,7 +2520,7 @@ footer.webview-footer button {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* CK Edito */
|
||||
/* CK Editor */
|
||||
|
||||
/* Insert text snippet: limit the width of the listed items to avoid overly long names */
|
||||
:root body.desktop div.ck-template-form li.ck-list__item .ck-template-form__text-part > span {
|
||||
@@ -2437,3 +2551,17 @@ iframe.print-iframe {
|
||||
.excalidraw.theme--dark canvas {
|
||||
--theme-filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
/* Scrolling container */
|
||||
|
||||
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scrolling-container > .note-detail.full-height,
|
||||
.scrolling-container > .note-list-widget.full-height {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
--native-titlebar-background: #00000000;
|
||||
--window-background-color-bgfx: transparent; /* When background effects enabled */
|
||||
|
||||
--main-background-color: #272727;
|
||||
--main-background-color: #242424;
|
||||
--main-text-color: #ccc;
|
||||
--main-border-color: #454545;
|
||||
--subtle-border-color: #313131;
|
||||
@@ -166,6 +166,9 @@
|
||||
--protected-session-active-icon-color: #8edd8e;
|
||||
--sync-status-error-pulse-color: #f47871;
|
||||
|
||||
--center-pane-vert-layout-background-color-bgfx: #0c0c0c69;
|
||||
--center-pane-horiz-layout-background-color-bgfx: #1e1e1ec7;
|
||||
|
||||
--right-pane-heading-color: gray;
|
||||
|
||||
--root-background: var(--left-pane-background-color);
|
||||
@@ -192,8 +195,8 @@
|
||||
--badge-background-color: #ffffff1a;
|
||||
--badge-text-color: var(--muted-text-color);
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #000000b3;
|
||||
--promoted-attribute-card-background-color: #ffffff21;
|
||||
--promoted-attribute-card-shadow: none;
|
||||
|
||||
--floating-button-shadow-color: #00000080;
|
||||
--floating-button-background-color: #494949d2;
|
||||
@@ -208,6 +211,8 @@
|
||||
--floating-button-hide-button-background: #00000029;
|
||||
--floating-button-hide-button-color: #ffffff63;
|
||||
|
||||
--right-pane-background-color: var(--main-background-color);
|
||||
--right-pane-background-color-bgfx: #0c0c0c24; /* Only for the vertical layout */
|
||||
--right-pane-item-hover-background: #ffffff26;
|
||||
--right-pane-item-hover-color: white;
|
||||
|
||||
@@ -225,10 +230,9 @@
|
||||
--code-block-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
|
||||
|
||||
--card-background-color: #ffffff12;
|
||||
--card-background-hover-color: #3c3c3c;
|
||||
--card-background-press-color: #464646;
|
||||
--card-border-color: #222222;
|
||||
--card-box-shadow: 0 0 12px rgba(0, 0, 0, 0.15);
|
||||
--card-background-hover-color: #ffffff20;
|
||||
--card-border-color: transparent;
|
||||
--card-box-shadow: none;
|
||||
|
||||
--calendar-color: var(--menu-text-color);
|
||||
--calendar-weekday-labels-color: var(--muted-text-color);
|
||||
@@ -295,3 +299,9 @@ body ::-webkit-calendar-picker-indicator {
|
||||
body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
border-color: var(--muted-text-color) !important;
|
||||
}
|
||||
|
||||
.tinted-quick-edit-dialog {
|
||||
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
|
||||
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
|
||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
|
||||
}
|
||||
@@ -159,6 +159,9 @@
|
||||
--protected-session-active-icon-color: #16b516;
|
||||
--sync-status-error-pulse-color: #ff5528;
|
||||
|
||||
--center-pane-vert-layout-background-color-bgfx: #ffffff75;
|
||||
--center-pane-horiz-layout-background-color-bgfx: #ffffffd6;
|
||||
|
||||
--right-pane-heading-color: gray;
|
||||
|
||||
--root-background: var(--left-pane-background-color);
|
||||
@@ -180,13 +183,13 @@
|
||||
--inactive-tab-hover-background-color: #00000016;
|
||||
--inactive-tab-text-color: #4e4e4e;
|
||||
|
||||
--alert-bar-background: #32637b29;
|
||||
--alert-bar-background: #f9cf2b29;
|
||||
|
||||
--badge-background-color: #00000011;
|
||||
--badge-text-color: var(--muted-text-color);
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #00000033;
|
||||
--promoted-attribute-card-background-color: #00000014;
|
||||
--promoted-attribute-card-shadow: none;
|
||||
|
||||
--floating-button-shadow-color: #00000042;
|
||||
--floating-button-background-color: #eaeaeacc;
|
||||
@@ -207,7 +210,9 @@
|
||||
--new-tab-button-hover-background: white;
|
||||
--new-tab-button-hover-color: black;
|
||||
|
||||
--right-pane-item-hover-background: #ececec;
|
||||
--right-pane-background-color: var(--main-background-color);
|
||||
--right-pane-background-color-bgfx: #ffffff9e; /* Only for the vertical layout */
|
||||
--right-pane-item-hover-background: #00000013;
|
||||
--right-pane-item-hover-color: inherit;
|
||||
|
||||
--scrollbar-thumb-color: #0000005c;
|
||||
@@ -223,12 +228,11 @@
|
||||
|
||||
--code-block-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1), 0px 0px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
--card-background-color: var(--accented-background-color);
|
||||
--card-background-hover-color: #f9f9f9;
|
||||
--card-background-press-color: #efefef;
|
||||
--card-border-color: #eaeaea;
|
||||
--card-background-color: #0000000d;
|
||||
--card-background-hover-color: #0000001c;
|
||||
--card-border-color: transparent;
|
||||
--card-shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--card-box-shadow: 0 0 12px var(--card-shadow-color);
|
||||
--card-box-shadow: none;
|
||||
|
||||
--calendar-color: var(--menu-text-color);
|
||||
--calendar-weekday-labels-color: var(--muted-text-color);
|
||||
@@ -271,3 +275,9 @@
|
||||
* This value is unset for gray tones. */
|
||||
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
|
||||
}
|
||||
|
||||
.tinted-quick-edit-dialog {
|
||||
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
|
||||
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
|
||||
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
|
||||
}
|
||||
@@ -82,6 +82,7 @@
|
||||
|
||||
/* Theme capabilities */
|
||||
--tab-note-icons: true;
|
||||
--allow-background-effects: true;
|
||||
|
||||
/* To ensure that a tree item's custom color remains sufficiently contrasted and readable,
|
||||
* the color is adjusted based on the current color scheme (light or dark). The lightness
|
||||
@@ -131,7 +132,8 @@ body.mobile .dropdown-menu .dropdown-menu {
|
||||
|
||||
body.desktop .dropdown-menu::before,
|
||||
:root .ck.ck-dropdown__panel::before,
|
||||
:root .excalidraw .popover::before {
|
||||
:root .excalidraw .popover::before,
|
||||
body.zen .note-split .ribbon-container .classic-toolbar-widget::before {
|
||||
content: "";
|
||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||
border-radius: var(--dropdown-border-radius);
|
||||
@@ -485,13 +487,21 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
--note-list-vertical-padding: 15px;
|
||||
background-color: var(--card-background-color);
|
||||
border: 1px solid var(--card-border-color) !important;
|
||||
box-shadow: 2px 3px 4px var(--card-shadow-color);
|
||||
border-radius: 12px;
|
||||
user-select: none;
|
||||
padding: 0;
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
:root .note-list .note-book-card:hover {
|
||||
background-color: var(--card-background-hover-color);
|
||||
transition: background-color 200ms ease-out;
|
||||
}
|
||||
|
||||
:root .note-list.grid-view .note-book-card:active {
|
||||
transform: scale(.98);
|
||||
}
|
||||
|
||||
.note-list.list-view .note-book-card {
|
||||
box-shadow: 0 0 3px var(--card-shadow-color);
|
||||
}
|
||||
@@ -500,10 +510,6 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.note-list-wrapper .note-book-card:active {
|
||||
background-color: var(--card-background-press-color);
|
||||
}
|
||||
|
||||
.note-list-wrapper .note-book-card a {
|
||||
color: inherit !important;
|
||||
}
|
||||
@@ -585,7 +591,6 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card:hover {
|
||||
background: var(--card-background-color) !important;
|
||||
filter: contrast(105%);
|
||||
}
|
||||
|
||||
|
||||
@@ -258,11 +258,6 @@
|
||||
border-inline-start: 1px solid var(--ck-color-toolbar-border);
|
||||
}
|
||||
|
||||
/* The last separator of the toolbar */
|
||||
:root .classic-toolbar-widget .ck.ck-toolbar__separator:last-of-type {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Heading dropdown */
|
||||
|
||||
:root .ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__panel .ck-list__item {
|
||||
@@ -680,3 +675,16 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
|
||||
.ck-content a.reference-link > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
* Read-only text content
|
||||
*/
|
||||
|
||||
.note-detail-readonly-text:focus-visible {
|
||||
outline: 2px solid var(--input-focus-outline-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.note-list-widget {
|
||||
outline: 0 !important;
|
||||
}
|
||||
@@ -101,7 +101,7 @@
|
||||
.sql-table-schemas-widget .sql-table-schemas button:hover,
|
||||
.sql-table-schemas-widget .sql-table-schemas button:active,
|
||||
.sql-table-schemas-widget .sql-table-schemas button:focus-visible {
|
||||
--background: var(--card-background-press-color);
|
||||
--background: var(--card-background-hover-color);
|
||||
--color: var(--main-text-color);
|
||||
}
|
||||
|
||||
@@ -123,8 +123,12 @@
|
||||
*/
|
||||
|
||||
/* The container */
|
||||
div.note-detail-empty {
|
||||
max-width: 70%;
|
||||
|
||||
.note-split.empty-note {
|
||||
--max-content-width: 70%;
|
||||
}
|
||||
|
||||
.note-split.empty-note div.note-detail {
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
@@ -148,7 +152,7 @@ div.note-detail-empty {
|
||||
--options-card-min-width: 500px;
|
||||
--options-card-max-width: 900px;
|
||||
--options-card-padding: 17px;
|
||||
--options-title-font-size: 1rem;
|
||||
--options-title-font-size: .75rem;
|
||||
--options-title-offset: 13px;
|
||||
}
|
||||
/* Create a gap at the top of the option pages */
|
||||
@@ -173,16 +177,19 @@ div.note-detail-empty {
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) {
|
||||
margin: auto;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--card-border-color) !important;
|
||||
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
|
||||
box-shadow: var(--card-box-shadow);
|
||||
border: 1px solid var(--card-border-color) !important;
|
||||
border-radius: 8px;
|
||||
background: var(--card-background-color);
|
||||
padding: var(--options-card-padding);
|
||||
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
|
||||
}
|
||||
|
||||
body.desktop .option-section:not(.tn-no-card) {
|
||||
body.prefers-centered-content .options-section:not(.tn-no-card) {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
body.desktop .options-section:not(.tn-no-card) {
|
||||
min-width: var(--options-card-min-width);
|
||||
max-width: var(--options-card-max-width);
|
||||
}
|
||||
@@ -193,9 +200,16 @@ body.desktop .option-section:not(.tn-no-card) {
|
||||
padding-bottom: var(--default-padding);
|
||||
}
|
||||
|
||||
.options-section:not(.tn-no-card) h4,
|
||||
.options-section:not(.tn-no-card) h5 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .4pt;
|
||||
}
|
||||
|
||||
|
||||
.options-section:not(.tn-no-card) h4 {
|
||||
font-size: var(--options-title-font-size);
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
color: var(--launcher-pane-text-color);
|
||||
margin-top: calc(-1 * var(--options-card-padding) - var(--options-title-font-size) - var(--options-title-offset)) !important;
|
||||
margin-bottom: calc(var(--options-title-offset) + var(--options-card-padding)) !important;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
div.promoted-attributes-container {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-inline-start: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--dropdown-backdrop-filter: blur(10px) saturate(6);
|
||||
--dropdown-backdrop-filter: blur(20px) saturate(6);
|
||||
--dropdown-border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -35,30 +35,53 @@ body.mobile {
|
||||
}
|
||||
|
||||
/* #region Mica */
|
||||
|
||||
body.background-effects.platform-win32 {
|
||||
/* Quirk: --background-material is read before "theme-supports-background-effects" class
|
||||
* is applied. Apply the matterial even if the theme doesn't support it. */
|
||||
--background-material: tabbed;
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32 {
|
||||
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
|
||||
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
|
||||
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
|
||||
--tab-background-color: var(--window-background-color-bgfx);
|
||||
--new-tab-button-background: var(--window-background-color-bgfx);
|
||||
--active-tab-background-color: var(--launcher-pane-horiz-background-color);
|
||||
--root-background: transparent;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32.layout-vertical {
|
||||
--left-pane-background-color: var(--window-background-color-bgfx);
|
||||
--background-material: mica;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32,
|
||||
body.background-effects.platform-win32 #root-widget {
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical {
|
||||
--left-pane-background-color: var(--window-background-color-bgfx);
|
||||
--center-pane-background-color-bgfx: var(--center-pane-vert-layout-background-color-bgfx);
|
||||
--right-pane-background-color: var(--right-pane-background-color-bgfx);
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal {
|
||||
--center-pane-background-color-bgfx: var(--center-pane-horiz-layout-background-color-bgfx);
|
||||
--gutter-color: var(--left-pane-background-color);
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32,
|
||||
body.background-effects.theme-supports-background-effects.platform-win32 #root-widget {
|
||||
background: var(--window-background-color-bgfx) !important;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32.layout-horizontal #horizontal-main-container,
|
||||
body.background-effects.platform-win32.layout-vertical #vertical-main-container {
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal #horizontal-main-container,
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical #vertical-main-container {
|
||||
background-color: var(--root-background);
|
||||
}
|
||||
|
||||
/* Note split with background effects */
|
||||
body.background-effects.theme-supports-background-effects.platform-win32 #center-pane .note-split.bgfx {
|
||||
--note-split-background-color: var(--center-pane-background-color-bgfx);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* Matches when the left pane is collapsed */
|
||||
@@ -72,9 +95,21 @@ body.layout-vertical #horizontal-main-container.left-pane-hidden #launcher-pane.
|
||||
border-inline-end: 2px solid var(--left-pane-collapsed-border-color);
|
||||
}
|
||||
|
||||
body.background-effects.zen #root-widget {
|
||||
--main-background-color: transparent;
|
||||
--root-background: transparent;
|
||||
/*
|
||||
* Zen mode
|
||||
*/
|
||||
|
||||
@keyframes zen-formatting-toolbar-entrance {
|
||||
from {
|
||||
transform: translateY(200%);
|
||||
} to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
||||
position: relative;
|
||||
animation: zen-formatting-toolbar-entrance 300ms ease-out;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1171,23 +1206,18 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
|
||||
* CENTER PANE
|
||||
*/
|
||||
|
||||
#center-pane {
|
||||
background: var(--main-background-color);
|
||||
}
|
||||
|
||||
.vertical-layout #center-pane {
|
||||
/* The first visible note split */
|
||||
.vertical-layout #center-pane .note-split:not(.visible ~ .visible) {
|
||||
border-radius: var(--center-pane-border-radius) 0 0 0;
|
||||
}
|
||||
|
||||
.note-split {
|
||||
#center-pane .note-split {
|
||||
padding-top: 2px;
|
||||
animation: note-entrance 100ms linear;
|
||||
/* will-change: opacity; -- causes some weird artifacts to the note menu in split view */
|
||||
background-color: var(--note-split-background-color, var(--main-background-color));
|
||||
}
|
||||
|
||||
.split-note-container-widget > .gutter {
|
||||
background: var(--root-background) !important;
|
||||
transition: background 150ms ease-out;
|
||||
body:not(.background-effects) #center-pane .note-split {
|
||||
animation: note-entrance 100ms linear;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1200,9 +1230,9 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
|
||||
|
||||
@keyframes note-entrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
filter: opacity(0);
|
||||
} to {
|
||||
opacity: 1;
|
||||
filter: opacity(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1328,8 +1358,7 @@ div.promoted-attribute-cell {
|
||||
--pa-card-padding-inline-end: 2px;
|
||||
--input-background-color: transparent;
|
||||
|
||||
box-shadow: 1px 1px 2px var(--promoted-attribute-card-shadow-color);
|
||||
|
||||
box-shadow: var(--promoted-attribute-card-shadow);
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
border-radius: 8px;
|
||||
@@ -1716,7 +1745,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
||||
*/
|
||||
|
||||
#right-pane {
|
||||
background: var(--main-background-color);
|
||||
background: var(--right-pane-background-color);
|
||||
}
|
||||
|
||||
#right-pane div.card-header {
|
||||
|
||||
@@ -520,9 +520,7 @@
|
||||
"max_content_width": {
|
||||
"max_width_unit": "بكسل",
|
||||
"title": "عرض المحتوى",
|
||||
"reload_button": "اعادة تحميل الواجهة",
|
||||
"max_width_label": "اقصى عرض للمحتوى",
|
||||
"reload_description": "تغييرات من خيارات المظهر"
|
||||
"max_width_label": "اقصى عرض للمحتوى"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"enabled": "مفعل",
|
||||
|
||||
@@ -39,7 +39,10 @@
|
||||
"help_on_tree_prefix": "有关树前缀的帮助",
|
||||
"prefix": "前缀: ",
|
||||
"save": "保存",
|
||||
"branch_prefix_saved": "分支前缀已保存。"
|
||||
"branch_prefix_saved": "分支前缀已保存。",
|
||||
"edit_branch_prefix_multiple": "编辑 {{count}} 个分支的前缀",
|
||||
"branch_prefix_saved_multiple": "已为 {{count}} 个分支保存分支前缀。",
|
||||
"affected_branches": "受影响的分支 {{count}}:"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "批量操作",
|
||||
@@ -1107,9 +1110,6 @@
|
||||
"title": "内容宽度",
|
||||
"default_description": "Trilium默认会限制内容的最大宽度以提高在宽屏中全屏时的可读性。",
|
||||
"max_width_label": "内容最大宽度(像素)",
|
||||
"apply_changes_description": "要应用内容宽度更改,请点击",
|
||||
"reload_button": "重载前端",
|
||||
"reload_description": "来自外观选项的更改",
|
||||
"max_width_unit": "像素"
|
||||
},
|
||||
"native_title_bar": {
|
||||
@@ -1917,7 +1917,7 @@
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "自定义日期/时间格式",
|
||||
"description": "通过<shortcut />或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
|
||||
"description": "自定义通过 <shortcut /> 或工具栏插入的日期和时间格式。有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
|
||||
"format_string": "日期/时间格式字符串:",
|
||||
"formatted_time": "格式化后日期/时间:"
|
||||
},
|
||||
@@ -2079,5 +2079,8 @@
|
||||
"edit-slide": "编辑此幻灯片",
|
||||
"start-presentation": "开始演示",
|
||||
"slide-overview": "切换幻灯片概览"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "删除笔记..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1104,9 +1104,6 @@
|
||||
"title": "Inhaltsbreite",
|
||||
"default_description": "Trilium begrenzt standardmäßig die maximale Inhaltsbreite, um die Lesbarkeit für maximierte Bildschirme auf Breitbildschirmen zu verbessern.",
|
||||
"max_width_label": "Maximale Inhaltsbreite in Pixel",
|
||||
"apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf",
|
||||
"reload_button": "Frontend neu laden",
|
||||
"reload_description": "Änderungen an den Darstellungsoptionen",
|
||||
"max_width_unit": "Pixel"
|
||||
},
|
||||
"native_title_bar": {
|
||||
|
||||
@@ -36,10 +36,13 @@
|
||||
},
|
||||
"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",
|
||||
"prefix": "Prefix: ",
|
||||
"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",
|
||||
@@ -1108,9 +1111,7 @@
|
||||
"default_description": "Trilium by default limits max content width to improve readability for maximized screens on wide screens.",
|
||||
"max_width_label": "Max content width",
|
||||
"max_width_unit": "pixels",
|
||||
"apply_changes_description": "To apply content width changes, click on",
|
||||
"reload_button": "reload frontend",
|
||||
"reload_description": "changes from appearance options"
|
||||
"centerContent": "Keep content centered"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Native Title Bar (requires app restart)",
|
||||
@@ -1636,6 +1637,12 @@
|
||||
"shared_locally": "This note is shared locally on {{- link}}.",
|
||||
"help_link": "For help visit <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
|
||||
},
|
||||
"read-only-info": {
|
||||
"read-only-note": "Currently viewing a read-only note.",
|
||||
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
|
||||
"auto-read-only-learn-more": "Learn more",
|
||||
"edit-note": "Edit note"
|
||||
},
|
||||
"note_types": {
|
||||
"text": "Text",
|
||||
"code": "Code",
|
||||
@@ -2034,6 +2041,9 @@
|
||||
"start-presentation": "Start presentation",
|
||||
"slide-overview": "Toggle an overview of the slides"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "Delete note..."
|
||||
},
|
||||
"command_palette": {
|
||||
"tree-action-name": "Tree: {{name}}",
|
||||
"export_note_title": "Export Note",
|
||||
|
||||
@@ -1107,10 +1107,7 @@
|
||||
"title": "Ancho del contenido",
|
||||
"default_description": "Trilium limita de forma predeterminada el ancho máximo del contenido para mejorar la legibilidad de ventanas maximizadas en pantallas anchas.",
|
||||
"max_width_label": "Ancho máximo del contenido en píxeles",
|
||||
"max_width_unit": "píxeles",
|
||||
"apply_changes_description": "Para aplicar cambios en el ancho del contenido, haga clic en",
|
||||
"reload_button": "recargar la interfaz",
|
||||
"reload_description": "cambios desde las opciones de apariencia"
|
||||
"max_width_unit": "píxeles"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barra de título nativa (requiere reiniciar la aplicación)",
|
||||
@@ -1592,7 +1589,7 @@
|
||||
"tree-context-menu": {
|
||||
"open-in-a-new-tab": "Abrir en nueva pestaña",
|
||||
"open-in-a-new-split": "Abrir en nueva división",
|
||||
"insert-note-after": "Insertar nota después de",
|
||||
"insert-note-after": "Insertar nota contigua",
|
||||
"insert-child-note": "Insertar subnota",
|
||||
"delete": "Eliminar",
|
||||
"search-in-subtree": "Buscar en subárbol",
|
||||
|
||||
@@ -1106,9 +1106,6 @@
|
||||
"title": "Largeur du contenu",
|
||||
"default_description": "Trilium limite par défaut la largeur maximale du contenu pour améliorer la lisibilité sur des écrans larges.",
|
||||
"max_width_label": "Largeur maximale du contenu en pixels",
|
||||
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
|
||||
"reload_button": "recharger l'interface",
|
||||
"reload_description": "changements par rapport aux options d'apparence",
|
||||
"max_width_unit": "Pixels"
|
||||
},
|
||||
"native_title_bar": {
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
"export_type_single": "Solo questa nota, senza le sottostanti",
|
||||
"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_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": {
|
||||
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
|
||||
@@ -1569,10 +1570,7 @@
|
||||
"title": "Larghezza del contenuto",
|
||||
"default_description": "Per impostazione predefinita, Trilium limita la larghezza massima del contenuto per migliorare la leggibilità sugli schermi più grandi.",
|
||||
"max_width_label": "Larghezza massima del contenuto",
|
||||
"max_width_unit": "pixel",
|
||||
"apply_changes_description": "Per applicare le modifiche alla larghezza del contenuto, fare clic su",
|
||||
"reload_button": "ricarica frontend",
|
||||
"reload_description": "modifiche dalle opzioni di aspetto"
|
||||
"max_width_unit": "pixel"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barra del titolo nativa (richiede il riavvio dell'app)",
|
||||
|
||||
@@ -39,7 +39,10 @@
|
||||
"edit_branch_prefix": "ブランチ接頭辞の編集",
|
||||
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
|
||||
"prefix": "接頭辞: ",
|
||||
"branch_prefix_saved": "ブランチの接頭辞が保存されました。"
|
||||
"branch_prefix_saved": "ブランチの接頭辞が保存されました。",
|
||||
"edit_branch_prefix_multiple": "{{count}} ブランチのブランチ接頭辞を編集",
|
||||
"branch_prefix_saved_multiple": "{{count}} 個のブランチのブランチ接頭辞が保存されました。",
|
||||
"affected_branches": "影響を受けるブランチ {{count}}:"
|
||||
},
|
||||
"global_menu": {
|
||||
"menu": "メニュー",
|
||||
@@ -830,13 +833,10 @@
|
||||
"theme_defined": "テーマが定義されました"
|
||||
},
|
||||
"max_content_width": {
|
||||
"reload_button": "フロントエンドをリロード",
|
||||
"title": "コンテンツ幅",
|
||||
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
|
||||
"max_width_label": "最大コンテンツ幅",
|
||||
"max_width_unit": "ピクセル",
|
||||
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください",
|
||||
"reload_description": "外観設定から変更"
|
||||
"max_width_unit": "ピクセル"
|
||||
},
|
||||
"theme": {
|
||||
"title": "アプリのテーマ",
|
||||
@@ -2079,5 +2079,8 @@
|
||||
"edit-slide": "このスライドを編集",
|
||||
"start-presentation": "プレゼンテーションを開始",
|
||||
"slide-overview": "スライドの概要を切り替え"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "ノートを削除..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1464,10 +1464,7 @@
|
||||
"title": "Szerokość zawartości",
|
||||
"default_description": "Trilium domyślnie ogranicza maksymalną szerokość zawartości, aby poprawić czytelność na zmaksymalizowanych ekranach o dużej szerokości.",
|
||||
"max_width_label": "Maksymalna szerokość zawartości",
|
||||
"max_width_unit": "piksele",
|
||||
"apply_changes_description": "Aby zastosować zmiany szerokości zawartości, kliknij na",
|
||||
"reload_button": "przeładuj frontend",
|
||||
"reload_description": "zmiany z opcji wyglądu"
|
||||
"max_width_unit": "piksele"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Natywny pasek tytułu (wymaga ponownego uruchomienia aplikacji)",
|
||||
|
||||
@@ -1082,10 +1082,7 @@
|
||||
"title": "Largura do Conteúdo",
|
||||
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em ecrãs largos.",
|
||||
"max_width_label": "Largura máxima do conteúdo",
|
||||
"max_width_unit": "pixels",
|
||||
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
|
||||
"reload_button": "recarregar frontend",
|
||||
"reload_description": "alterações de opções de aparência"
|
||||
"max_width_unit": "pixels"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barra de Título Nativa (requer recarregar a app)",
|
||||
|
||||
@@ -1304,9 +1304,6 @@
|
||||
"title": "Largura do Conteúdo",
|
||||
"max_width_label": "Largura máxima do conteúdo",
|
||||
"max_width_unit": "pixels",
|
||||
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
|
||||
"reload_button": "recarregar frontend",
|
||||
"reload_description": "alterações de opções de aparência",
|
||||
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em telas wide."
|
||||
},
|
||||
"native_title_bar": {
|
||||
|
||||
@@ -796,12 +796,9 @@
|
||||
"modal_body_text": "Din cauza limitărilor la nivel de navigator, nu este posibilă citirea clipboard-ului din JavaScript. Inserați Markdown-ul pentru a-l importa în caseta de mai jos și dați clic pe butonul Import"
|
||||
},
|
||||
"max_content_width": {
|
||||
"apply_changes_description": "Pentru a aplica schimbările de lățime a conținutului, dați click pe",
|
||||
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
|
||||
"max_width_label": "Lungimea maximă a conținutului",
|
||||
"max_width_unit": "pixeli",
|
||||
"reload_button": "reîncarcă interfața",
|
||||
"reload_description": "schimbări din opțiunile de afișare",
|
||||
"title": "Lățime conținut"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
|
||||
@@ -1203,11 +1203,8 @@
|
||||
"max_content_width": {
|
||||
"max_width_unit": "пикселей",
|
||||
"title": "Ширина контентной области",
|
||||
"reload_button": "перезагрузить интерфейс",
|
||||
"default_description": "Trilium по умолчанию ограничивает максимальную ширину контента, чтобы улучшить читаемость на широких экранах.",
|
||||
"max_width_label": "Максимальная ширина контентной области",
|
||||
"apply_changes_description": "Чтобы применить изменения, нажмите на",
|
||||
"reload_description": "изменения в параметрах внешнего вида"
|
||||
"max_width_label": "Максимальная ширина контентной области"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"enabled": "включено",
|
||||
|
||||
@@ -39,7 +39,10 @@
|
||||
"help_on_tree_prefix": "有關樹前綴的說明",
|
||||
"prefix": "前綴: ",
|
||||
"save": "儲存",
|
||||
"branch_prefix_saved": "已儲存分支前綴。"
|
||||
"branch_prefix_saved": "已儲存分支前綴。",
|
||||
"edit_branch_prefix_multiple": "編輯 {{count}} 個分支的前綴",
|
||||
"branch_prefix_saved_multiple": "已為 {{count}} 個分支儲存分支前綴。",
|
||||
"affected_branches": "受影響的分支 ({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "批次操作",
|
||||
@@ -1104,9 +1107,6 @@
|
||||
"title": "內容寬度",
|
||||
"default_description": "Trilium 預設會限制內容的最大寬度以提高在寬螢幕中全螢幕時的可讀性。",
|
||||
"max_width_label": "內容最大寬度(像素)",
|
||||
"apply_changes_description": "要套用內容寬度更改,請點擊",
|
||||
"reload_button": "重新載入前端",
|
||||
"reload_description": "來自外觀選項的更改",
|
||||
"max_width_unit": "像素"
|
||||
},
|
||||
"native_title_bar": {
|
||||
@@ -2079,5 +2079,8 @@
|
||||
"edit-slide": "編輯此投影片",
|
||||
"start-presentation": "開始簡報",
|
||||
"slide-overview": "切換投影片概覽"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "刪除筆記…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1204,10 +1204,7 @@
|
||||
"title": "Ширина вмісту",
|
||||
"default_description": "Trilium за замовчуванням обмежує максимальну ширину вмісту, щоб поліпшити читабельність на широкоформатних екранах у режимі максимального розширення.",
|
||||
"max_width_label": "Максимальна ширина вмісту",
|
||||
"max_width_unit": "пікселів",
|
||||
"apply_changes_description": "Щоб застосувати зміни ширини вмісту, натисніть на",
|
||||
"reload_button": "перезавантажити інтерфейс",
|
||||
"reload_description": "зміни в параметрах зовнішнього вигляду"
|
||||
"max_width_unit": "пікселів"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Нативний рядок заголовка (потрібен перезапуск)",
|
||||
|
||||
26
apps/client/src/types-fancytree.d.ts
vendored
26
apps/client/src/types-fancytree.d.ts
vendored
@@ -215,6 +215,30 @@ declare namespace Fancytree {
|
||||
enableUpdate(enabled: boolean): void;
|
||||
}
|
||||
|
||||
interface FancytreeNodeData {
|
||||
noteId: string;
|
||||
parentNoteId: string;
|
||||
branchId: string;
|
||||
isProtected: boolean;
|
||||
noteType: NoteType;
|
||||
}
|
||||
|
||||
interface FancytreeNewNode extends FancytreeNodeData {
|
||||
title: string;
|
||||
extraClasses: string;
|
||||
icon: string;
|
||||
refKey: string;
|
||||
/** True if this node is loaded on demand, i.e. on first expansion. */
|
||||
lazy: boolean;
|
||||
/** Folder nodes have different default icons and click behavior. Note: Also non-folders may have children. */
|
||||
folder: boolean;
|
||||
/** Use isExpanded(), setExpanded() to access this property. */
|
||||
expanded: boolean;
|
||||
/** Node id (must be unique inside the tree) */
|
||||
key: string;
|
||||
children?: FancytreeNewNode[];
|
||||
}
|
||||
|
||||
/** A FancytreeNode represents the hierarchical data model and operations. */
|
||||
interface FancytreeNode {
|
||||
// #region Properties
|
||||
@@ -227,7 +251,7 @@ declare namespace Fancytree {
|
||||
/** Display name (may contain HTML) */
|
||||
title: string;
|
||||
/** Contains all extra data that was passed on node creation */
|
||||
data: any;
|
||||
data: FancytreeNodeData;
|
||||
/** Array of child nodes. For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array to define a node that has no children. */
|
||||
children: FancytreeNode[];
|
||||
/** Use isExpanded(), setExpanded() to access this property. */
|
||||
|
||||
@@ -23,6 +23,24 @@ export class CssVarReader {
|
||||
return (!isNaN(number.valueOf()) ? number.valueOf() : defaultValue)
|
||||
}
|
||||
|
||||
asBoolean(defaultValue?: boolean) {
|
||||
let value = this.value.toLocaleLowerCase().trim();
|
||||
let result: boolean | undefined;
|
||||
|
||||
switch (value) {
|
||||
case "true":
|
||||
case "1":
|
||||
result = true;
|
||||
break;
|
||||
case "false":
|
||||
case "0":
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return (result !== undefined) ? result : defaultValue;
|
||||
}
|
||||
|
||||
asEnum<T>(enumType: T, defaultValue?: T[keyof T]): T[keyof T] | undefined {
|
||||
let result: T[keyof T] | undefined;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.floating-buttons-children,
|
||||
.show-floating-buttons {
|
||||
position: absolute;
|
||||
top: var(--floating-buttons-vert-offset, 10px);
|
||||
top: var(--floating-buttons-vert-offset, 14px);
|
||||
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from "i18next";
|
||||
import "./FloatingButtons.css";
|
||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean } from "./react/hooks";
|
||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "./react/hooks";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { ParentComponent } from "./react/react_utils";
|
||||
import { EventData, EventNames } from "../components/app_context";
|
||||
@@ -20,6 +20,7 @@ interface FloatingButtonsProps {
|
||||
* properly handle rounded corners, as defined by the --border-radius CSS variable.
|
||||
*/
|
||||
export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
||||
const [ top, setTop ] = useState(0);
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||
@@ -47,8 +48,14 @@ export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
||||
const [ visible, setVisible ] = useState(true);
|
||||
useEffect(() => setVisible(true), [ note ]);
|
||||
|
||||
useTriliumEvent("contentSafeMarginChanged", (e) => {
|
||||
if (e.noteContext === noteContext) {
|
||||
setTop(e.top);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="floating-buttons no-print">
|
||||
<div className="floating-buttons no-print" style={{top}}>
|
||||
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
|
||||
{context && items.map((Component) => (
|
||||
<Component {...context} />
|
||||
|
||||
@@ -4,7 +4,7 @@ import Component from "../components/component";
|
||||
import NoteContext from "../components/note_context";
|
||||
import FNote from "../entities/fnote";
|
||||
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
|
||||
import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
|
||||
import server from "../services/server";
|
||||
@@ -13,8 +13,6 @@ import toast from "../services/toast";
|
||||
import { t } from "../services/i18n";
|
||||
import { copyImageReferenceToClipboard } from "../services/image";
|
||||
import tree from "../services/tree";
|
||||
import protected_session_holder from "../services/protected_session_holder";
|
||||
import options from "../services/options";
|
||||
import { getHelpUrlForNote } from "../services/in_app_help";
|
||||
import froca from "../services/froca";
|
||||
import NoteLink from "./react/NoteLink";
|
||||
@@ -101,48 +99,26 @@ function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingBut
|
||||
/>
|
||||
}
|
||||
|
||||
function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ animationClass, setAnimationClass ] = useState("");
|
||||
const [ isEnabled, setIsEnabled ] = useState(false);
|
||||
function EditButton({ note, noteContext }: FloatingButtonContext) {
|
||||
const [animationClass, setAnimationClass] = useState("");
|
||||
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
const isReadOnlyInfoBarDismissed = false; // TODO
|
||||
|
||||
useEffect(() => {
|
||||
noteContext.isReadOnly().then(isReadOnly => {
|
||||
setIsEnabled(
|
||||
isDefaultViewMode
|
||||
&& (!note.isProtected || protected_session_holder.isProtectedSessionAvailable())
|
||||
&& !options.is("databaseReadonly")
|
||||
&& isReadOnly
|
||||
);
|
||||
});
|
||||
}, [ note ]);
|
||||
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
|
||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||
setIsEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
// make the edit button stand out on the first display, otherwise
|
||||
// it's difficult to notice that the note is readonly
|
||||
useEffect(() => {
|
||||
if (isEnabled) {
|
||||
if (isReadOnly) {
|
||||
setAnimationClass("bx-tada bx-lg");
|
||||
setTimeout(() => {
|
||||
setAnimationClass("");
|
||||
}, 1700);
|
||||
}
|
||||
}, [ isEnabled ]);
|
||||
}, [ isReadOnly ]);
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
return !!isReadOnly && isReadOnlyInfoBarDismissed && <FloatingButton
|
||||
text={t("edit_button.edit_this_note")}
|
||||
icon="bx bx-pencil"
|
||||
className={animationClass}
|
||||
onClick={() => {
|
||||
if (noteContext.viewScope) {
|
||||
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
|
||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext });
|
||||
}
|
||||
}}
|
||||
onClick={() => enableEditing()}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
19
apps/client/src/widgets/ReadOnlyNoteInfoBar.css
Normal file
19
apps/client/src/widgets/ReadOnlyNoteInfoBar.css
Normal file
@@ -0,0 +1,19 @@
|
||||
body.zen div.read-only-note-info-bar-widget {
|
||||
width: fit-content;
|
||||
max-width: var(--max-content-width);
|
||||
border-radius: 8px;
|
||||
border: unset;
|
||||
margin: 0 auto 10px auto;
|
||||
}
|
||||
|
||||
.read-only-note-info-bar-widget-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
:root div.read-only-note-info-bar-widget button {
|
||||
white-space: nowrap;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
36
apps/client/src/widgets/ReadOnlyNoteInfoBar.tsx
Normal file
36
apps/client/src/widgets/ReadOnlyNoteInfoBar.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import "./ReadOnlyNoteInfoBar.css";
|
||||
import { t } from "../services/i18n";
|
||||
import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks"
|
||||
import Button from "./react/Button";
|
||||
import InfoBar from "./react/InfoBar";
|
||||
|
||||
export default function ReadOnlyNoteInfoBar(props: {}) {
|
||||
const {note, noteContext} = useNoteContext();
|
||||
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
||||
|
||||
return <InfoBar className="read-only-note-info-bar-widget"
|
||||
type={(isExplicitReadOnly ? "subtle" : "prominent")}
|
||||
style={{display: (!isReadOnly) ? "none" : undefined}}>
|
||||
|
||||
<div class="read-only-note-info-bar-widget-content">
|
||||
{(isExplicitReadOnly) ? (
|
||||
<div>{t("read-only-info.read-only-note")}</div>
|
||||
) : (
|
||||
<div>
|
||||
{t("read-only-info.auto-read-only-note")}
|
||||
|
||||
<a class="tn-link"
|
||||
href="https://docs.triliumnotes.org/user-guide/concepts/notes/read-only-notes#automatic-read-only-mode">
|
||||
|
||||
{t("read-only-info.auto-read-only-learn-more")}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button text={t("read-only-info.edit-note")}
|
||||
icon="bx-pencil" onClick={() => enableEditing()} />
|
||||
</div>
|
||||
</InfoBar>
|
||||
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
.note-list-widget {
|
||||
min-height: 0;
|
||||
max-width: var(--max-content-width); /* Inherited from .note-split */
|
||||
|
||||
overflow: auto;
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
body.prefers-centered-content .note-list-widget:not(.full-height) {
|
||||
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.note-list-widget .note-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
32
apps/client/src/widgets/collections/board/data.spec.ts
Normal file
32
apps/client/src/widgets/collections/board/data.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { it, describe, expect } from "vitest";
|
||||
import { buildNote } from "../../../test/easy-froca";
|
||||
import { getBoardData } from "./data";
|
||||
import FBranch from "../../../entities/fbranch";
|
||||
import froca from "../../../services/froca";
|
||||
|
||||
describe("Board data", () => {
|
||||
it("deduplicates cloned notes", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Board",
|
||||
"#collection": "",
|
||||
"#viewType": "board",
|
||||
children: [
|
||||
{ id: "note1", title: "First note", "#status": "To Do" },
|
||||
{ id: "note2", title: "Second note", "#status": "In progress" },
|
||||
{ id: "note3", title: "Third note", "#status": "Done" }
|
||||
]
|
||||
});
|
||||
const branch = new FBranch(froca, {
|
||||
branchId: "note1_note2",
|
||||
notePosition: 10,
|
||||
fromSearchNote: false,
|
||||
noteId: "note2",
|
||||
parentNoteId: "note1"
|
||||
});
|
||||
froca.branches["note1_note2"] = branch;
|
||||
froca.getNoteFromCache("note1").addChild("note2", "note1_note2", false);
|
||||
const data = await getBoardData(parentNote, "status", {}, false);
|
||||
const noteIds = Array.from(data.byColumn.values()).flat().map(item => item.note.noteId);
|
||||
expect(noteIds.length).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
|
||||
const byColumn: ColumnMap = new Map();
|
||||
|
||||
// First, scan all notes to find what columns actually exist
|
||||
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived);
|
||||
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived, new Set<string>());
|
||||
|
||||
// Get all columns that exist in the notes
|
||||
const columnsFromNotes = [...byColumn.keys()];
|
||||
@@ -61,26 +61,28 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
|
||||
};
|
||||
}
|
||||
|
||||
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean) {
|
||||
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean, seenNoteIds: Set<string>) {
|
||||
for (const branch of branches) {
|
||||
const note = await branch.getNote();
|
||||
if (!note || (!includeArchived && note.isArchived)) continue;
|
||||
|
||||
if (note.type !== "search" && note.hasChildren()) {
|
||||
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived);
|
||||
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived, seenNoteIds);
|
||||
}
|
||||
|
||||
const group = note.getLabelValue(groupByColumn);
|
||||
if (!group) {
|
||||
if (!group || seenNoteIds.has(note.noteId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!byColumn.has(group)) {
|
||||
byColumn.set(group, []);
|
||||
}
|
||||
|
||||
byColumn.get(group)!.push({
|
||||
branch,
|
||||
note
|
||||
});
|
||||
seenNoteIds.add(note.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,6 @@ export async function changeEvent(note: FNote, { startDate, endDate, startTime,
|
||||
startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
|
||||
endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
|
||||
|
||||
if (startTime && endTime) {
|
||||
setAttribute(note, "label", startAttribute, startTime);
|
||||
setAttribute(note, "label", endAttribute, endTime);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
.calendar-view {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 10px;
|
||||
@@ -67,6 +68,7 @@
|
||||
}
|
||||
|
||||
body.desktop:not(.zen) .calendar-view .calendar-header {
|
||||
padding-block-start: 4px;
|
||||
padding-inline-end: 5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import Button, { ButtonGroup } from "../../react/Button";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { RefObject } from "preact";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import { openCalendarContextMenu } from "./context_menu";
|
||||
|
||||
interface CalendarViewData {
|
||||
|
||||
@@ -106,7 +107,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||
const locale = useLocale();
|
||||
|
||||
const { eventDidMount } = useEventDisplayCustomization();
|
||||
const { eventDidMount } = useEventDisplayCustomization(note);
|
||||
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
||||
|
||||
// React to changes.
|
||||
@@ -196,11 +197,11 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
|
||||
}
|
||||
|
||||
function useLocale() {
|
||||
const [ locale ] = useTriliumOption("locale");
|
||||
const [ formattingLocale ] = useTriliumOption("formattingLocale");
|
||||
const [ calendarLocale, setCalendarLocale ] = useState<LocaleInput>();
|
||||
|
||||
useEffect(() => {
|
||||
const correspondingLocale = LOCALE_MAPPINGS[locale];
|
||||
const correspondingLocale = LOCALE_MAPPINGS[formattingLocale];
|
||||
if (correspondingLocale) {
|
||||
correspondingLocale().then((locale) => setCalendarLocale(locale.default));
|
||||
} else {
|
||||
@@ -253,7 +254,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
function useEventDisplayCustomization() {
|
||||
function useEventDisplayCustomization(parentNote: FNote) {
|
||||
const eventDidMount = useCallback((e: EventMountArg) => {
|
||||
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
||||
|
||||
@@ -302,6 +303,11 @@ function useEventDisplayCustomization() {
|
||||
}
|
||||
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
||||
}
|
||||
|
||||
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
|
||||
const noteId = e.event.extendedProps.noteId;
|
||||
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
|
||||
});
|
||||
}, []);
|
||||
return { eventDidMount };
|
||||
}
|
||||
|
||||
62
apps/client/src/widgets/containers/content-header.ts
Normal file
62
apps/client/src/widgets/containers/content-header.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { EventData } from "../../components/app_context";
|
||||
import BasicWidget from "../basic_widget";
|
||||
import Container from "./container";
|
||||
import NoteContext from "../../components/note_context";
|
||||
|
||||
export default class ContentHeader extends Container<BasicWidget> {
|
||||
|
||||
noteContext?: NoteContext;
|
||||
thisElement?: HTMLElement;
|
||||
parentElement?: HTMLElement;
|
||||
resizeObserver: ResizeObserver;
|
||||
currentHeight: number = 0;
|
||||
currentSafeMargin: number = NaN;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.css("contain", "unset");
|
||||
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
|
||||
}
|
||||
|
||||
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
|
||||
this.noteContext = noteContext;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.parentElement = this.parent?.$widget.get(0);
|
||||
if (!this.parentElement) {
|
||||
console.warn("No parent set for <ContentHeader>.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.thisElement = this.$widget.get(0)!;
|
||||
|
||||
this.resizeObserver.observe(this.thisElement);
|
||||
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
|
||||
}
|
||||
|
||||
updateSafeMargin() {
|
||||
const newSafeMargin = Math.max(this.currentHeight - this.parentElement!.scrollTop, 0);
|
||||
|
||||
if (newSafeMargin !== this.currentSafeMargin) {
|
||||
this.currentSafeMargin = newSafeMargin;
|
||||
|
||||
this.triggerEvent("contentSafeMarginChanged", {
|
||||
top: newSafeMargin,
|
||||
noteContext: this.noteContext!
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onResize(entries: ResizeObserverEntry[]) {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === this.thisElement) {
|
||||
this.currentHeight = entry.contentRect.height;
|
||||
this.updateSafeMargin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { EventData } from "../../components/app_context.js";
|
||||
import { LOCALES } from "@triliumnext/commons";
|
||||
import { readCssVar } from "../../utils/css-var.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import options from "../../services/options.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import { LOCALES } from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||
@@ -30,9 +31,11 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
|
||||
}
|
||||
|
||||
this.#setMotion(options.is("motionEnabled"));
|
||||
this.#setShadows(options.is("shadowsEnabled"));
|
||||
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
||||
this.#setMaxContentWidth();
|
||||
this.#setMotion();
|
||||
this.#setShadows();
|
||||
this.#setBackdropEffects();
|
||||
this.#setThemeCapabilities();
|
||||
this.#setLocaleAndDirection(options.get("locale"));
|
||||
|
||||
return super.render();
|
||||
@@ -40,15 +43,21 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isOptionReloaded("motionEnabled")) {
|
||||
this.#setMotion(options.is("motionEnabled"));
|
||||
this.#setMotion();
|
||||
}
|
||||
|
||||
if (loadResults.isOptionReloaded("shadowsEnabled")) {
|
||||
this.#setShadows(options.is("shadowsEnabled"));
|
||||
this.#setShadows();
|
||||
}
|
||||
|
||||
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
|
||||
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
||||
this.#setBackdropEffects();
|
||||
}
|
||||
|
||||
if (loadResults.isOptionReloaded("maxContentWidth")
|
||||
|| loadResults.isOptionReloaded("centerContent")) {
|
||||
|
||||
this.#setMaxContentWidth();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,19 +67,38 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
||||
}
|
||||
|
||||
#setMotion(enabled: boolean) {
|
||||
#setMaxContentWidth() {
|
||||
const width = Math.max(options.getInt("maxContentWidth") || 0, 640);
|
||||
document.body.style.setProperty("--preferred-max-content-width", `${width}px`);
|
||||
|
||||
document.body.classList.toggle("prefers-centered-content", options.is("centerContent"));
|
||||
}
|
||||
|
||||
#setMotion() {
|
||||
const enabled = options.is("motionEnabled");
|
||||
document.body.classList.toggle("motion-disabled", !enabled);
|
||||
jQuery.fx.off = !enabled;
|
||||
}
|
||||
|
||||
#setShadows(enabled: boolean) {
|
||||
#setShadows() {
|
||||
const enabled = options.is("shadowsEnabled");
|
||||
document.body.classList.toggle("shadows-disabled", !enabled);
|
||||
}
|
||||
|
||||
#setBackdropEffects(enabled: boolean) {
|
||||
#setBackdropEffects() {
|
||||
const enabled = options.is("backdropEffectsEnabled");
|
||||
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
|
||||
}
|
||||
|
||||
#setThemeCapabilities() {
|
||||
// Supports background effects
|
||||
|
||||
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
|
||||
.asBoolean(false);
|
||||
|
||||
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
|
||||
}
|
||||
|
||||
#setLocaleAndDirection(locale: string) {
|
||||
const correspondingLocale = LOCALES.find(l => l.id === locale);
|
||||
document.body.lang = locale;
|
||||
|
||||
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 { useTriliumEvent } from "../react/hooks.jsx";
|
||||
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() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ branch, setBranch ] = useState<FBranch>();
|
||||
const [ branches, setBranches ] = useState<FBranch[]>([]);
|
||||
const [ prefix, setPrefix ] = useState("");
|
||||
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();
|
||||
if (!notePath) {
|
||||
return;
|
||||
@@ -29,8 +41,8 @@ export default function BranchPrefixDialog() {
|
||||
return;
|
||||
}
|
||||
|
||||
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
if (!newBranchId) {
|
||||
const branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
if (!branchId) {
|
||||
return;
|
||||
}
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
@@ -38,25 +50,46 @@ export default function BranchPrefixDialog() {
|
||||
return;
|
||||
}
|
||||
|
||||
const newBranch = froca.getBranch(newBranchId);
|
||||
setBranch(newBranch);
|
||||
setPrefix(newBranch?.prefix ?? "");
|
||||
branchIds = [branchId];
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
if (!branch) {
|
||||
if (branches.length === 0) {
|
||||
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);
|
||||
}
|
||||
|
||||
const isSingleBranch = branches.length === 1;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
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"
|
||||
onShown={() => branchInput.current?.focus()}
|
||||
onHidden={() => setShown(false)}
|
||||
@@ -69,9 +102,27 @@ export default function BranchPrefixDialog() {
|
||||
<div class="input-group">
|
||||
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
||||
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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -80,3 +131,8 @@ async function savePrefix(branchId: string, prefix: string) {
|
||||
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
||||
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 }));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ import TypeWidget from "../type_widgets/type_widget.js";
|
||||
const TPL = /*html*/`\
|
||||
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
<style>
|
||||
/** Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. */
|
||||
body.popup-editor-open > .modal-backdrop { z-index: 998; }
|
||||
body.popup-editor-open .popup-editor-dialog { z-index: 999; }
|
||||
body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
|
||||
|
||||
body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||
max-width: 75vw;
|
||||
}
|
||||
@@ -57,6 +62,7 @@ const TPL = /*html*/`\
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="quick-edit-dialog-wrapper">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -71,6 +77,7 @@ const TPL = /*html*/`\
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -79,6 +86,7 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
||||
private noteContext: NoteContext;
|
||||
private $modalHeader!: JQuery<HTMLElement>;
|
||||
private $modalBody!: JQuery<HTMLElement>;
|
||||
private $wrapper!: JQuery<HTMLDivElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -93,6 +101,7 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
||||
const $newWidget = $(TPL);
|
||||
this.$modalHeader = $newWidget.find(".modal-title");
|
||||
this.$modalBody = $newWidget.find(".modal-body");
|
||||
this.$wrapper = $newWidget.find(".quick-edit-dialog-wrapper");
|
||||
|
||||
const children = this.$widget.children();
|
||||
this.$modalHeader.append(children[0]);
|
||||
@@ -112,17 +121,27 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
||||
}
|
||||
});
|
||||
|
||||
const colorClass = this.noteContext.note?.getColorClass();
|
||||
const wrapperElement = this.$wrapper.get(0)!;
|
||||
|
||||
if (colorClass) {
|
||||
wrapperElement.className = "quick-edit-dialog-wrapper " + colorClass;
|
||||
} else {
|
||||
wrapperElement.className = "quick-edit-dialog-wrapper";
|
||||
}
|
||||
|
||||
const customHue = getComputedStyle(wrapperElement).getPropertyValue("--custom-color-hue");
|
||||
if (customHue) {
|
||||
/* Apply the tinted-dialog class only if the custom color CSS class specifies a hue */
|
||||
wrapperElement.classList.add("tinted-quick-edit-dialog");
|
||||
}
|
||||
|
||||
const activeEl = document.activeElement;
|
||||
if (activeEl && "blur" in activeEl) {
|
||||
(activeEl as HTMLElement).blur();
|
||||
}
|
||||
|
||||
$dialog.on("shown.bs.modal", async () => {
|
||||
// Reduce the z-index of modals so that ckeditor popups are properly shown on top of it.
|
||||
// The backdrop instance is not shared so it's OK to make a one-off modification.
|
||||
$("body > .modal-backdrop").css("z-index", "998");
|
||||
$dialog.css("z-index", "999");
|
||||
|
||||
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
|
||||
this.setVisibility(true);
|
||||
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
|
||||
@@ -143,9 +162,12 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
||||
if (visible) {
|
||||
$bodyItems.fadeIn();
|
||||
this.$modalHeader.children().show();
|
||||
document.body.classList.add("popup-editor-open");
|
||||
|
||||
} else {
|
||||
$bodyItems.hide();
|
||||
this.$modalHeader.children().hide();
|
||||
document.body.classList.remove("popup-editor-open");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,12 +39,14 @@ const TPL = /*html*/`
|
||||
<div class="note-detail">
|
||||
<style>
|
||||
.note-detail {
|
||||
max-width: var(--max-content-width); /* Inherited from .note-split */
|
||||
font-family: var(--detail-font-family);
|
||||
font-size: var(--detail-font-size);
|
||||
}
|
||||
|
||||
.note-detail.full-height {
|
||||
height: 100%;
|
||||
body.prefers-centered-content .note-detail {
|
||||
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
|
||||
margin-inline: auto;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
@@ -173,14 +173,6 @@ interface ExpandedSubtreeResponse {
|
||||
branchIds: string[];
|
||||
}
|
||||
|
||||
interface Node extends Fancytree.NodeData {
|
||||
noteId: string;
|
||||
parentNoteId: string;
|
||||
branchId: string;
|
||||
isProtected: boolean;
|
||||
noteType: NoteType;
|
||||
}
|
||||
|
||||
interface RefreshContext {
|
||||
noteIdsToUpdate: Set<string>;
|
||||
noteIdsToReload: Set<string>;
|
||||
@@ -769,7 +761,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
prepareChildren(parentNote: FNote) {
|
||||
utils.assertArguments(parentNote);
|
||||
|
||||
const noteList: Node[] = [];
|
||||
const noteList: Fancytree.FancytreeNewNode[] = [];
|
||||
|
||||
const hideArchivedNotes = this.hideArchivedNotes;
|
||||
|
||||
@@ -837,7 +829,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
const isFolder = note.isFolder();
|
||||
|
||||
const node: Node = {
|
||||
const node: Fancytree.FancytreeNewNode = {
|
||||
noteId: note.noteId,
|
||||
parentNoteId: branch.parentNoteId,
|
||||
branchId: branch.branchId,
|
||||
@@ -849,7 +841,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
refKey: note.noteId,
|
||||
lazy: true,
|
||||
folder: isFolder,
|
||||
expanded: branch.isExpanded && note.type !== "search",
|
||||
expanded: !!branch.isExpanded && note.type !== "search",
|
||||
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||
};
|
||||
|
||||
@@ -911,7 +903,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
return extraClasses.join(" ");
|
||||
}
|
||||
|
||||
/** @returns {FancytreeNode[]} */
|
||||
getSelectedNodes(stopOnParents = false) {
|
||||
return this.tree.getSelectedNodes(stopOnParents);
|
||||
}
|
||||
@@ -1532,7 +1523,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
// Automatically expand the hoisted note by default
|
||||
const node = this.getActiveNode();
|
||||
if (node?.data.noteId === this.noteContext.hoistedNoteId){
|
||||
if (node && node.data.noteId === this.noteContext.hoistedNoteId){
|
||||
this.setExpanded(node.data.branchId, true);
|
||||
}
|
||||
}
|
||||
@@ -1591,6 +1582,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
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) {
|
||||
if (node.data.noteId === "root") {
|
||||
return false;
|
||||
|
||||
@@ -52,6 +52,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
|
||||
const note = this.noteContext?.note;
|
||||
if (!note) {
|
||||
this.$widget.addClass("bgfx empty-note");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
|
||||
this.$widget.addClass(utils.getNoteTypeClass(note.type));
|
||||
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
|
||||
|
||||
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
|
||||
this.$widget.toggleClass("protected", note.isProtected);
|
||||
|
||||
const noteLanguage = note?.getLabelValue("language");
|
||||
@@ -70,7 +71,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#isFullWidthNote(note: FNote) {
|
||||
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
|
||||
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,15 +27,16 @@ interface CKEditorOpts {
|
||||
onClick?: (e: MouseEvent, pos?: ModelPosition | null) => void;
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
onBlur?: () => void;
|
||||
onInitialized?: (editorInstance: CKTextEditor) => void;
|
||||
}
|
||||
|
||||
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, ...restProps }: CKEditorOpts) {
|
||||
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, onInitialized, ...restProps }: CKEditorOpts) {
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||
const textEditorRef = useRef<CKTextEditor>(null);
|
||||
useImperativeHandle(apiRef, () => {
|
||||
return {
|
||||
focus() {
|
||||
editorContainerRef.current?.focus();
|
||||
textEditorRef.current?.editing.view.focus();
|
||||
textEditorRef.current?.model.change((writer) => {
|
||||
const documentRoot = textEditorRef.current?.editing.model.document.getRoot();
|
||||
if (documentRoot) {
|
||||
@@ -83,6 +84,8 @@ export default function CKEditor({ apiRef, currentValue, editor, config, disable
|
||||
if (currentValue) {
|
||||
textEditor.setData(currentValue);
|
||||
}
|
||||
|
||||
onInitialized?.(textEditor);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
23
apps/client/src/widgets/react/InfoBar.css
Normal file
23
apps/client/src/widgets/react/InfoBar.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.info-bar {
|
||||
--link-color: currentColor;
|
||||
|
||||
margin-top: 4px;
|
||||
contain: unset !important;
|
||||
padding: 8px 20px;
|
||||
color: var(--read-only-note-info-bar-color);
|
||||
font-size: .9em;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.info-bar-prominent {
|
||||
background: var(--alert-bar-background, var(--accented-background-color));
|
||||
}
|
||||
|
||||
.info-bar-subtle {
|
||||
color: var(--muted-text-color);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-block: 0;
|
||||
margin-inline: 10px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
19
apps/client/src/widgets/react/InfoBar.tsx
Normal file
19
apps/client/src/widgets/react/InfoBar.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import "./InfoBar.css";
|
||||
import { ComponentChildren, CSSProperties } from "preact";
|
||||
|
||||
export type InfoBarParams = {
|
||||
type: "prominent" | "subtle",
|
||||
className: string;
|
||||
style: CSSProperties
|
||||
children: ComponentChildren;
|
||||
};
|
||||
|
||||
export default function InfoBar(props: InfoBarParams) {
|
||||
return <div className={`info-bar ${props.className} info-bar-${props.type}`} style={props.style}>
|
||||
{props?.children}
|
||||
</div>
|
||||
}
|
||||
|
||||
InfoBar.defaultProps = {
|
||||
type: "prominent"
|
||||
} as InfoBarParams
|
||||
@@ -1,25 +1,26 @@
|
||||
import { Inputs, MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { CommandListenerData, EventData, EventNames } from "../../components/app_context";
|
||||
import { ParentComponent } from "./react_utils";
|
||||
import SpacedUpdate from "../../services/spaced_update";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
import { DragData } from "../note_tree";
|
||||
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
|
||||
import options, { type OptionValue } from "../../services/options";
|
||||
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import FBlob from "../../entities/fblob";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
import { Inputs, MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { ParentComponent } from "./react_utils";
|
||||
import { RefObject, VNode } from "preact";
|
||||
import { Tooltip } from "bootstrap";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
import { ViewMode } from "../../services/link";
|
||||
import appContext, { CommandListenerData, EventData, EventNames } from "../../components/app_context";
|
||||
import attributes from "../../services/attributes";
|
||||
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
|
||||
import Component from "../../components/component";
|
||||
import FBlob from "../../entities/fblob";
|
||||
import FNote from "../../entities/fnote";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
import Mark from "mark.js";
|
||||
import { DragData } from "../note_tree";
|
||||
import Component from "../../components/component";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
import options, { type OptionValue } from "../../services/options";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import SpacedUpdate from "../../services/spaced_update";
|
||||
import toast, { ToastOptions } from "../../services/toast";
|
||||
import { ViewMode } from "../../services/link";
|
||||
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
|
||||
|
||||
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
@@ -198,6 +199,7 @@ export function useNoteContext() {
|
||||
const [ notePath, setNotePath ] = useState<string | null | undefined>();
|
||||
const [ note, setNote ] = useState<FNote | null | undefined>();
|
||||
const [ , setViewMode ] = useState<ViewMode>();
|
||||
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
|
||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -217,6 +219,11 @@ export function useNoteContext() {
|
||||
setRefreshCounter(refreshCounter + 1);
|
||||
}
|
||||
});
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
|
||||
if (eventNoteContext.ntxId === noteContext?.ntxId) {
|
||||
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
|
||||
}
|
||||
});
|
||||
|
||||
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
|
||||
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||
@@ -230,7 +237,8 @@ export function useNoteContext() {
|
||||
viewScope: noteContext?.viewScope,
|
||||
componentId: parentComponent.componentId,
|
||||
noteContext,
|
||||
parentComponent
|
||||
parentComponent,
|
||||
isReadOnlyTemporarilyDisabled
|
||||
};
|
||||
|
||||
}
|
||||
@@ -701,3 +709,51 @@ export function useResizeObserver(ref: RefObject<HTMLElement>, callback: () => v
|
||||
return () => observer.disconnect();
|
||||
}, [ callback, ref ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the current note is in read-only mode, while an editing mode is available,
|
||||
* and provides a way to switch to editing mode.
|
||||
*/
|
||||
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const enableEditing = useCallback(() => {
|
||||
if (noteContext?.viewScope) {
|
||||
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
|
||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
|
||||
}
|
||||
}, [noteContext]);
|
||||
|
||||
useEffect(() => {
|
||||
if (note && noteContext) {
|
||||
isNoteReadOnly(note, noteContext).then((readOnly) => {
|
||||
setIsReadOnly(readOnly);
|
||||
});
|
||||
}
|
||||
}, [note, noteContext]);
|
||||
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
|
||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||
setIsReadOnly(false);
|
||||
}
|
||||
});
|
||||
|
||||
return {isReadOnly, enableEditing};
|
||||
}
|
||||
|
||||
async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
||||
|
||||
if (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.is("databaseReadonly")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (noteContext.viewScope?.viewMode !== "default" || !await noteContext.isReadOnly()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function EditedNotesTab({ note }: TabContext) {
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}))}
|
||||
}), " ")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote"
|
||||
import dialog from "../../services/dialog";
|
||||
import { t } from "../../services/i18n"
|
||||
import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import ws from "../../services/ws";
|
||||
import ActionButton from "../react/ActionButton"
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { t } from "../../services/i18n"
|
||||
import { useContext } from "preact/hooks";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import { useIsNoteReadOnly } from "../react/hooks";
|
||||
import ActionButton from "../react/ActionButton"
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import branches from "../../services/branches";
|
||||
import dialog from "../../services/dialog";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import FNote from "../../entities/fnote"
|
||||
import NoteContext from "../../components/note_context";
|
||||
import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import ws from "../../services/ws";
|
||||
|
||||
interface NoteActionsProps {
|
||||
note?: FNote;
|
||||
@@ -52,6 +53,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
const isMac = getIsMac();
|
||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type);
|
||||
const isSearchOrBook = ["search", "book"].includes(note.type);
|
||||
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@@ -59,8 +61,14 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
className="note-actions"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
iconAction
|
||||
>
|
||||
iconAction>
|
||||
|
||||
{isReadOnly && <>
|
||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||
command={() => enableEditing()} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
|
||||
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
|
||||
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
|
||||
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||
import "./style.css";
|
||||
|
||||
import { numberObjectsInPlace } from "../../services/utils";
|
||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||
import { EventNames } from "../../components/app_context";
|
||||
import NoteActions from "./NoteActions";
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
@@ -11,30 +11,47 @@ import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||
|
||||
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
||||
|
||||
interface ComputedTab extends Indexed<TabConfiguration> {
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
export default function Ribbon() {
|
||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
|
||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const titleContext: TitleContext = { note };
|
||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
||||
const computedTabs = useMemo(
|
||||
() => TAB_CONFIGURATION.map(tab => {
|
||||
const shouldShow = typeof tab.show === "boolean" ? tab.show : tab.show?.(titleContext);
|
||||
return {
|
||||
const [ computedTabs, setComputedTabs ] = useState<ComputedTab[]>();
|
||||
const titleContext: TitleContext = useMemo(() => ({
|
||||
note,
|
||||
noteContext
|
||||
}), [ note, noteContext ]);
|
||||
|
||||
async function refresh() {
|
||||
const computedTabs: ComputedTab[] = [];
|
||||
for (const tab of TAB_CONFIGURATION) {
|
||||
const shouldShow = await shouldShowTab(tab.show, titleContext);
|
||||
computedTabs.push({
|
||||
...tab,
|
||||
shouldShow
|
||||
shouldShow: !!shouldShow
|
||||
});
|
||||
}
|
||||
}),
|
||||
[ titleContext, note, noteType ]);
|
||||
setComputedTabs(computedTabs);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
||||
|
||||
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
||||
useEffect(() => {
|
||||
if (!computedTabs) return;
|
||||
const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)));
|
||||
setActiveTabIndex(tabToActivate?.index);
|
||||
}, [ note?.noteId ]);
|
||||
}, [ computedTabs, note?.noteId ]);
|
||||
|
||||
// Register keyboard shortcuts.
|
||||
const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []);
|
||||
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
|
||||
if (!computedTabs) return;
|
||||
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
|
||||
if (correspondingTab) {
|
||||
if (activeTabIndex !== correspondingTab.index) {
|
||||
@@ -51,7 +68,7 @@ export default function Ribbon() {
|
||||
<>
|
||||
<div className="ribbon-top-row">
|
||||
<div className="ribbon-tab-container">
|
||||
{computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
|
||||
{computedTabs && computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
|
||||
shouldShow && <RibbonTab
|
||||
icon={icon}
|
||||
title={typeof title === "string" ? title : title(titleContext)}
|
||||
@@ -74,7 +91,7 @@ export default function Ribbon() {
|
||||
</div>
|
||||
|
||||
<div className="ribbon-body-container">
|
||||
{computedTabs.map(tab => {
|
||||
{computedTabs && computedTabs.map(tab => {
|
||||
const isActive = tab.index === activeTabIndex;
|
||||
if (!isActive && !tab.stayInDom) {
|
||||
return;
|
||||
@@ -129,3 +146,9 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
|
||||
)
|
||||
}
|
||||
|
||||
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {
|
||||
if (showConfig === null || showConfig === undefined) return true;
|
||||
if (typeof showConfig === "boolean") return showConfig;
|
||||
if ("then" in showConfig) return await showConfig(context);
|
||||
return showConfig(context);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
{
|
||||
title: t("classic_editor_toolbar.title"),
|
||||
icon: "bx bx-text",
|
||||
show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic",
|
||||
show: async ({ note, noteContext }) => note?.type === "text"
|
||||
&& options.get("textNoteEditorType") === "ckeditor-classic"
|
||||
&& !(await noteContext?.isReadOnly()),
|
||||
toggleCommand: "toggleRibbonTabClassicEditor",
|
||||
content: FormattingToolbar,
|
||||
activate: true,
|
||||
|
||||
@@ -238,11 +238,6 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
}
|
||||
});
|
||||
|
||||
// Focus on show.
|
||||
useEffect(() => {
|
||||
setTimeout(() => editorRef.current?.focus(), 0);
|
||||
}, []);
|
||||
|
||||
// Interaction with CKEditor.
|
||||
useLegacyImperativeHandlers(useMemo(() => ({
|
||||
loadReferenceLinkTitle: async ($el: JQuery<HTMLElement>, href: string) => {
|
||||
@@ -363,6 +358,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
}}
|
||||
onKeyDown={() => attributeDetailWidget.hide()}
|
||||
onBlur={() => save()}
|
||||
onInitialized={() => editorRef.current?.focus()}
|
||||
disableNewlines disableSpellcheck
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useNoteContext } from "../../react/hooks";
|
||||
import { TabContext, TitleContext } from "../ribbon-interface";
|
||||
import { TabContext } from "../ribbon-interface";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { RIBBON_TAB_DEFINITIONS } from "../RibbonDefinition";
|
||||
import { shouldShowTab } from "../Ribbon";
|
||||
|
||||
interface StandaloneRibbonAdapterProps {
|
||||
component: (props: TabContext) => ComponentChildren;
|
||||
@@ -16,10 +17,11 @@ export default function StandaloneRibbonAdapter({ component }: StandaloneRibbonA
|
||||
const Component = component;
|
||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
|
||||
const definition = useMemo(() => RIBBON_TAB_DEFINITIONS.find(def => def.content === component), [ component ]);
|
||||
const [ shown, setShown ] = useState(unwrapShown(definition?.show, { note }));
|
||||
const [ shown, setShown ] = useState<boolean | null | undefined>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShown(unwrapShown(definition?.show, { note }));
|
||||
if (!definition) return;
|
||||
shouldShowTab(definition.show, { note, noteContext }).then(setShown);
|
||||
}, [ note ]);
|
||||
|
||||
return (
|
||||
@@ -35,9 +37,3 @@ export default function StandaloneRibbonAdapter({ component }: StandaloneRibbonA
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function unwrapShown(value: boolean | ((context: TitleContext) => boolean | null | undefined) | undefined, context: TitleContext) {
|
||||
if (!value) return true;
|
||||
if (typeof value === "boolean") return value;
|
||||
return !!value(context);
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@ export interface TabContext {
|
||||
|
||||
export interface TitleContext {
|
||||
note: FNote | null | undefined;
|
||||
noteContext: NoteContext | undefined;
|
||||
}
|
||||
|
||||
export interface TabConfiguration {
|
||||
title: string | ((context: TitleContext) => string);
|
||||
icon: string;
|
||||
content: (context: TabContext) => VNode | false;
|
||||
show: boolean | ((context: TitleContext) => boolean | null | undefined);
|
||||
show: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined);
|
||||
toggleCommand?: KeyboardActionNames;
|
||||
activate?: boolean | ((context: TitleContext) => boolean);
|
||||
/**
|
||||
|
||||
7
apps/client/src/widgets/shared_info.css
Normal file
7
apps/client/src/widgets/shared_info.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.shared-info-widget {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.shared-info-widget button {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "./shared_info.css";
|
||||
import { t } from "../services/i18n";
|
||||
import Alert from "./react/Alert";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useNoteContext, useTriliumEvent, useTriliumOption } from "./react/hooks";
|
||||
import FNote from "../entities/fnote";
|
||||
import attributes from "../services/attributes";
|
||||
import RawHtml from "./react/RawHtml";
|
||||
import FNote from "../entities/fnote";
|
||||
import HelpButton from "./react/HelpButton";
|
||||
import InfoBar from "./react/InfoBar";
|
||||
import RawHtml from "./react/RawHtml";
|
||||
|
||||
export default function SharedInfo() {
|
||||
const { note } = useNoteContext();
|
||||
@@ -35,7 +36,7 @@ export default function SharedInfo() {
|
||||
link = `${location.protocol}//${host}${location.pathname}share/${shareId}`;
|
||||
}
|
||||
|
||||
setLink(`<a href="${link}" class="external">${link}</a>`);
|
||||
setLink(`<a href="${link}" class="external tn-link">${link}</a>`);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
@@ -48,20 +49,14 @@ export default function SharedInfo() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Alert className="shared-info-widget" type="warning" style={{
|
||||
contain: "none",
|
||||
margin: "10px",
|
||||
padding: "10px",
|
||||
fontWeight: "bold",
|
||||
display: !link ? "none" : undefined
|
||||
}}>
|
||||
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
|
||||
{link && (
|
||||
<RawHtml html={syncServerHost
|
||||
? t("shared_info.shared_publicly", { link })
|
||||
: t("shared_info.shared_locally", { link })} />
|
||||
)}
|
||||
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
|
||||
</Alert>
|
||||
</InfoBar>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ import protected_session_holder from "../../services/protected_session_holder.js
|
||||
const TPL = /*html*/`
|
||||
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
|
||||
<style>
|
||||
.canvas-widget {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.excalidraw .App-menu_top .buttonList {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -284,7 +284,8 @@ function SmoothScrollEnabledOption() {
|
||||
}
|
||||
|
||||
function MaxContentWidth() {
|
||||
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
|
||||
const [maxContentWidth, setMaxContentWidth] = useTriliumOption("maxContentWidth");
|
||||
const [centerContent, setCenterContent] = useTriliumOptionBool("centerContent");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("max_content_width.title")}>
|
||||
@@ -300,9 +301,9 @@ function MaxContentWidth() {
|
||||
</FormGroup>
|
||||
</Column>
|
||||
|
||||
<p>
|
||||
{t("max_content_width.apply_changes_description")} <Button text={t("max_content_width.reload_button")} size="micro" onClick={reloadFrontendApp} />
|
||||
</p>
|
||||
<FormCheckbox label={t("max_content_width.centerContent")}
|
||||
currentValue={centerContent}
|
||||
onChange={setCenterContent} />
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
|
||||
|
||||
return (
|
||||
tokens.length ? (
|
||||
<div style={{ overflow: "auto", height: "500px"}}>
|
||||
<div style={{ overflow: "auto"}}>
|
||||
<table className="table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -72,8 +72,8 @@ function EditorFeatures() {
|
||||
return (
|
||||
<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="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
||||
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_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.slash_commands_description")} />
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const TPL = /*html*/`
|
||||
body.heading-style-underline .note-detail-readonly-text h6 { border-bottom: 1px solid var(--main-border-color); }
|
||||
|
||||
.note-detail-readonly-text {
|
||||
|
||||
padding-inline-start: 24px;
|
||||
padding-top: 10px;
|
||||
font-family: var(--detail-font-family);
|
||||
|
||||
@@ -103,7 +103,7 @@ const linkOverlays = [
|
||||
];
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-detail-relation-map note-detail-printable">
|
||||
<div class="note-detail-relation-map full-height note-detail-printable">
|
||||
<div class="relation-map-wrapper">
|
||||
<div class="relation-map-container"></div>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +108,8 @@ const config: ForgeConfig = {
|
||||
"--share=network",
|
||||
// System notifications with libnotify
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
// System tray
|
||||
"--talk-name=org.kde.StatusNotifierWatcher"
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/desktop",
|
||||
"version": "0.99.3",
|
||||
"version": "0.99.5",
|
||||
"description": "Build your personal knowledge base with Trilium Notes",
|
||||
"private": true,
|
||||
"main": "src/main.ts",
|
||||
@@ -16,7 +16,7 @@
|
||||
"build": "tsx scripts/build.ts",
|
||||
"start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
|
||||
"electron-forge:make": "pnpm build && electron-forge make dist",
|
||||
"electron-forge:make-flatpak": "pnpm build && electron-forge make dist --targets=@electron-forge/maker-flatpak",
|
||||
"electron-forge:make-flatpak": "pnpm build && DEBUG=* electron-forge make dist --targets=@electron-forge/maker-flatpak",
|
||||
"electron-forge:package": "pnpm build && electron-forge package dist",
|
||||
"electron-forge:start": "pnpm build && electron-forge start dist",
|
||||
"e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test"
|
||||
@@ -35,7 +35,7 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "38.5.0",
|
||||
"electron": "38.6.0",
|
||||
"@electron-forge/cli": "7.10.2",
|
||||
"@electron-forge/maker-deb": "7.10.2",
|
||||
"@electron-forge/maker-dmg": "7.10.2",
|
||||
|
||||
@@ -39,11 +39,16 @@ async function main() {
|
||||
app.commandLine.appendSwitch("disable-smooth-scrolling");
|
||||
}
|
||||
|
||||
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
||||
// See https://github.com/electron/electron/issues/46538 for more info.
|
||||
if (process.platform === "linux") {
|
||||
app.setName(PRODUCT_NAME);
|
||||
|
||||
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
||||
// See https://github.com/electron/electron/issues/46538 for more info.
|
||||
app.commandLine.appendSwitch("gtk-version", "3");
|
||||
|
||||
// Enable global shortcuts in Flatpak
|
||||
// the app runs in a Wayland session.
|
||||
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
|
||||
}
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
|
||||
3
apps/edit-docs/demo/!!!meta.json
vendored
3
apps/edit-docs/demo/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.99.2",
|
||||
"appVersion": "0.99.3",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
@@ -2700,6 +2700,7 @@
|
||||
}
|
||||
],
|
||||
"format": "html",
|
||||
"dataFileName": "Note Types.html",
|
||||
"attachments": [],
|
||||
"dirFileName": "Note Types",
|
||||
"children": [
|
||||
|
||||
2
apps/edit-docs/demo/navigation.html
vendored
2
apps/edit-docs/demo/navigation.html
vendored
@@ -270,7 +270,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Note Types
|
||||
<li><a href="root/Trilium%20Demo/Note%20Types.html" target="detail">Note Types</a>
|
||||
<ul>
|
||||
<li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a>
|
||||
</li>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<h2>☑️ Tasks</h2>
|
||||
|
||||
<ul>
|
||||
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
|
||||
</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">
|
||||
<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"
|
||||
height="150">
|
||||
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg"
|
||||
width="150" height="150">
|
||||
</figure>
|
||||
<p><strong>Welcome to Trilium Notes!</strong>
|
||||
|
||||
</p>
|
||||
<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.
|
||||
@@ -26,22 +25,17 @@
|
||||
you wish.</p>
|
||||
<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>
|
||||
|
||||
</p>
|
||||
<h2>Cleanup</h2>
|
||||
|
||||
<p>Once you're finished with experimenting and want to cleanup these pages,
|
||||
you can simply delete them all.</p>
|
||||
<h2>Formatting</h2>
|
||||
|
||||
<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
|
||||
<a
|
||||
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
|
||||
<h3>Lists</h3>
|
||||
|
||||
<p><strong>Ordered:</strong>
|
||||
|
||||
</p>
|
||||
<ol>
|
||||
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
|
||||
@@ -56,7 +50,6 @@
|
||||
</li>
|
||||
</ol>
|
||||
<p><strong>Unordered:</strong>
|
||||
|
||||
</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
|
||||
@@ -67,7 +60,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Block quotes</h3>
|
||||
|
||||
<blockquote>
|
||||
<p>Whereof one cannot speak, thereof one must be silent”</p>
|
||||
<p>– Ludwig Wittgenstein</p>
|
||||
@@ -75,9 +67,9 @@
|
||||
<hr>
|
||||
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</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
|
||||
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>
|
||||
</body>
|
||||
|
||||
@@ -21,8 +21,12 @@
|
||||
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() {
|
||||
|
||||
|
||||
|
||||
alert("Hello world");
|
||||
|
||||
|
||||
|
||||
}</code></pre>
|
||||
<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,
|
||||
|
||||
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>
|
||||
|
||||
<div class="ck-content">
|
||||
<p>This is a simple TODO/Task manager. You can see some description and explanation
|
||||
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a>
|
||||
</p>
|
||||
<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
|
||||
information on how it works.</p>
|
||||
<p>Please note that this is meant as scripting example only and feature/bug
|
||||
support is very limited.</p>
|
||||
</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><pre><code class="language-text-x-sh">#!/bin/bash
|
||||
|
||||
|
||||
|
||||
# This script opens 4 terminal windows.
|
||||
|
||||
|
||||
|
||||
i="0"
|
||||
|
||||
|
||||
|
||||
while [ $i -lt 4 ]
|
||||
|
||||
|
||||
|
||||
do
|
||||
|
||||
|
||||
|
||||
xterm &
|
||||
|
||||
|
||||
|
||||
i=$[$i+1]
|
||||
|
||||
|
||||
|
||||
done</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "38.5.0",
|
||||
"electron": "38.6.0",
|
||||
"fs-extra": "11.3.2"
|
||||
},
|
||||
"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-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"
|
||||
}
|
||||
}
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/server",
|
||||
"version": "0.99.3",
|
||||
"version": "0.99.5",
|
||||
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
|
||||
"private": true,
|
||||
"main": "./src/main.ts",
|
||||
@@ -67,7 +67,7 @@
|
||||
"@types/xml2js": "0.4.14",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "1.13.1",
|
||||
"axios": "1.13.2",
|
||||
"bindings": "1.5.0",
|
||||
"bootstrap": "5.3.8",
|
||||
"chardet": "2.1.1",
|
||||
@@ -81,7 +81,7 @@
|
||||
"debounce": "3.0.0",
|
||||
"debug": "4.4.3",
|
||||
"ejs": "3.1.10",
|
||||
"electron": "38.5.0",
|
||||
"electron": "38.6.0",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
@@ -97,7 +97,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.6.0",
|
||||
"i18next": "25.6.1",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
@@ -105,17 +105,17 @@
|
||||
"is-svg": "6.1.0",
|
||||
"jimp": "1.6.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"marked": "16.4.1",
|
||||
"marked": "16.4.2",
|
||||
"mime-types": "3.0.1",
|
||||
"multer": "2.0.2",
|
||||
"normalize-strings": "1.1.1",
|
||||
"ollama": "0.6.2",
|
||||
"openai": "6.7.0",
|
||||
"openai": "6.8.1",
|
||||
"rand-token": "1.0.1",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sax": "1.4.1",
|
||||
"sax": "1.4.3",
|
||||
"serve-favicon": "2.5.1",
|
||||
"stream-throttle": "0.1.3",
|
||||
"strip-bom": "5.0.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"tmp": "0.2.5",
|
||||
"turndown": "7.2.2",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "7.1.12",
|
||||
"vite": "7.2.2",
|
||||
"ws": "8.18.3",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.2.0"
|
||||
|
||||
@@ -20,10 +20,10 @@ fi
|
||||
# Debug output
|
||||
echo "Selected Arch: $ARCH"
|
||||
|
||||
# Set Node.js version and architecture-specific filename
|
||||
NODE_VERSION=22.16.0
|
||||
|
||||
script_dir=$(realpath $(dirname $0))
|
||||
|
||||
# Set Node.js version and architecture-specific filename
|
||||
NODE_VERSION=$(cat "../../.nvmrc")
|
||||
BUILD_DIR="$script_dir/../dist"
|
||||
DIST_DIR="$script_dir/../out"
|
||||
|
||||
|
||||
Binary file not shown.
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user