mirror of
https://github.com/zadam/trilium.git
synced 2025-12-15 04:39:53 +01:00
Compare commits
399 Commits
bugfix/tit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e2111a8f | ||
|
|
c5e4c484dc | ||
|
|
75a6dece7a | ||
|
|
5c0e7736d6 | ||
|
|
2562ecd055 | ||
|
|
aaaa47b575 | ||
|
|
21d82ec1d7 | ||
|
|
5af8444cac | ||
|
|
cd82c34b93 | ||
|
|
d182659d62 | ||
|
|
171f428b9d | ||
|
|
da4ca9c804 | ||
|
|
c019341503 | ||
|
|
7234f04b56 | ||
|
|
1998cbc005 | ||
|
|
5914073c3f | ||
|
|
d5aadf2604 | ||
|
|
1fe22f940b | ||
|
|
0cdaf70efe | ||
|
|
8174c65243 | ||
|
|
2645801277 | ||
|
|
fb8c31cb9c | ||
|
|
7287dbd64f | ||
|
|
6569d64931 | ||
|
|
e9f3216926 | ||
|
|
ca0af9646d | ||
|
|
92dfafd1ff | ||
|
|
d04dde3b97 | ||
|
|
4c520c6df3 | ||
|
|
65d6ed1cdc | ||
|
|
3352a92445 | ||
|
|
bc8c55b8fb | ||
|
|
7660914eb8 | ||
|
|
869aec778c | ||
|
|
255726dcc4 | ||
|
|
9969000807 | ||
|
|
3b909fd739 | ||
|
|
ad08fb8132 | ||
|
|
8d536a6040 | ||
|
|
2b1bc8e2b9 | ||
|
|
563194ff6c | ||
|
|
0c9ff4dae4 | ||
|
|
b10e7f1811 | ||
|
|
f93ad499e2 | ||
|
|
87a51251ca | ||
|
|
b56e5b2483 | ||
|
|
476c162016 | ||
|
|
4182f6043a | ||
|
|
aa528c65b7 | ||
|
|
4998560e31 | ||
|
|
86f36922c4 | ||
|
|
4f617b86d3 | ||
|
|
b28527e10d | ||
|
|
fbb8924ebf | ||
|
|
f68c9b751f | ||
|
|
8091f02b16 | ||
|
|
f4c68d115b | ||
|
|
6c70d6b9ae | ||
|
|
1ea12567a3 | ||
|
|
2d16ab7a70 | ||
|
|
a228ba5273 | ||
|
|
d0477e9ebf | ||
|
|
c99907972d | ||
|
|
b9ebc7d7ea | ||
|
|
4f9e2c5eca | ||
|
|
ab1f8ee5ae | ||
|
|
89276ad51a | ||
|
|
eca533a517 | ||
|
|
0be578c517 | ||
|
|
198b315602 | ||
|
|
6474abc983 | ||
|
|
2137dbe849 | ||
|
|
b7b46703d9 | ||
|
|
d2d96a1421 | ||
|
|
cfcc309e5a | ||
|
|
7d87ec942e | ||
|
|
4def13272f | ||
|
|
c4f914bb7b | ||
|
|
6bf213a0b0 | ||
|
|
694cd2bc7c | ||
|
|
3851a94400 | ||
|
|
e296416a54 | ||
|
|
0bd89a659c | ||
|
|
0ada6523a8 | ||
|
|
56570d7ba1 | ||
|
|
0ffdedcfa6 | ||
|
|
f391bb8eec | ||
|
|
7000076961 | ||
|
|
e0f6ba808c | ||
|
|
4c2fe8a846 | ||
|
|
2ea23368bc | ||
|
|
87666005a6 | ||
|
|
7666f44b7a | ||
|
|
470f6e5334 | ||
|
|
a2b007874b | ||
|
|
9946d8c6b9 | ||
|
|
02fab16475 | ||
|
|
5145ce2d23 | ||
|
|
e06abe6e5b | ||
|
|
50a847777e | ||
|
|
4473f80d73 | ||
|
|
70c918c9c6 | ||
|
|
0939975631 | ||
|
|
0ef90c6165 | ||
|
|
cef14a3b19 | ||
|
|
61d3141bce | ||
|
|
f040a0b6d1 | ||
|
|
e9dfec88c9 | ||
|
|
6fa97c845a | ||
|
|
f686d9ecd0 | ||
|
|
621ebe4396 | ||
|
|
ac2a566685 | ||
|
|
ac3d57d5da | ||
|
|
9ab5eef984 | ||
|
|
912f90accf | ||
|
|
6463b0dcaa | ||
|
|
0b45fb6764 | ||
|
|
330d71847b | ||
|
|
60c8f0c78b | ||
|
|
fcbd1ab0b1 | ||
|
|
3549bfb328 | ||
|
|
c97038fffd | ||
|
|
15b5885982 | ||
|
|
6aa8d9fbf9 | ||
|
|
eccf4620ac | ||
|
|
f08fbe9bb2 | ||
|
|
bfa87af489 | ||
|
|
a7899b7505 | ||
|
|
e80b5cddcd | ||
|
|
db12f9b8dc | ||
|
|
f4c95195c9 | ||
|
|
e2cbff7b3a | ||
|
|
98a3c8150c | ||
|
|
447e09fec1 | ||
|
|
7d2a1bb2e5 | ||
|
|
40fcf79778 | ||
|
|
88a779bbdb | ||
|
|
db04514769 | ||
|
|
23062470f5 | ||
|
|
5bad043ed5 | ||
|
|
4ab8af0995 | ||
|
|
1a65c5e13e | ||
|
|
fc08946038 | ||
|
|
4d6dba06ad | ||
|
|
d7887fe25f | ||
|
|
81dd50e752 | ||
|
|
fe13065ef8 | ||
|
|
eb02330fdf | ||
|
|
738fa6fd0e | ||
|
|
0c1c7e4f8e | ||
|
|
9eb9b66398 | ||
|
|
9db046b401 | ||
|
|
914272eee0 | ||
|
|
2b7e203bcc | ||
|
|
a61ddedc0b | ||
|
|
60fc34ffac | ||
|
|
685109556c | ||
|
|
45927053f3 | ||
|
|
5d438a877b | ||
|
|
870499bc3a | ||
|
|
c6d97e3d4b | ||
|
|
efff38b116 | ||
|
|
1b725175c6 | ||
|
|
6eff62f73f | ||
|
|
95d2160c76 | ||
|
|
2b195155ed | ||
|
|
28e9abc8bb | ||
|
|
0162b9d441 | ||
|
|
0545b929e1 | ||
|
|
d2b32ff5af | ||
|
|
2d3776cd5f | ||
|
|
2638963171 | ||
|
|
24ed97f65d | ||
|
|
c099634e39 | ||
|
|
12be14e6cf | ||
|
|
4dc773c1a3 | ||
|
|
31c5323fd9 | ||
|
|
74b6e7bf63 | ||
|
|
34025fa646 | ||
|
|
df9554194a | ||
|
|
4e1188484d | ||
|
|
2f44b9dc59 | ||
|
|
9ee3c48485 | ||
|
|
78b9c94829 | ||
|
|
4c8225ed73 | ||
|
|
88aad6d351 | ||
|
|
d99d701095 | ||
|
|
61fe27abbe | ||
|
|
24cd5006d5 | ||
|
|
726d6aad65 | ||
|
|
bd9fe14a6c | ||
|
|
792a10ace5 | ||
|
|
e9ac69b8e5 | ||
|
|
c76ff2d371 | ||
|
|
8ab9e30404 | ||
|
|
53b7d93efb | ||
|
|
00df3c3d1f | ||
|
|
e766b82418 | ||
|
|
9f4757af5b | ||
|
|
1a9fb34a6e | ||
|
|
a1513a3567 | ||
|
|
0de67b6a69 | ||
|
|
fec5ee9335 | ||
|
|
b540111fa4 | ||
|
|
0eed72b888 | ||
|
|
0856d3dbdf | ||
|
|
a9b453c27a | ||
|
|
fa8287269f | ||
|
|
1eee471018 | ||
|
|
c3829f82ab | ||
|
|
a51820f5df | ||
|
|
68591fb511 | ||
|
|
3795ce2143 | ||
|
|
3561a4f14d | ||
|
|
84cda001aa | ||
|
|
481127a560 | ||
|
|
c708e7cd61 | ||
|
|
fee0268792 | ||
|
|
953593c9d4 | ||
|
|
5ff60e53cb | ||
|
|
b38ee36fae | ||
|
|
38a415faf0 | ||
|
|
1e26864842 | ||
|
|
4b74ad5577 | ||
|
|
e5696713de | ||
|
|
2e44397c88 | ||
|
|
5d19881981 | ||
|
|
1711384eaa | ||
|
|
9897efe4af | ||
|
|
884578ea95 | ||
|
|
e404e76299 | ||
|
|
1db54cba3e | ||
|
|
77e3cc4021 | ||
|
|
242c63dfb4 | ||
|
|
f5440576b5 | ||
|
|
b020365af4 | ||
|
|
25e5bf0b86 | ||
|
|
19b32dd3a6 | ||
|
|
1ab89d0db0 | ||
|
|
6e8e10323f | ||
|
|
58bc5dc66a | ||
|
|
db42bb603b | ||
|
|
cb382c9537 | ||
|
|
a4b79a2dc9 | ||
|
|
0f867e02c4 | ||
|
|
ab1b4b37f4 | ||
|
|
5a1d138f29 | ||
|
|
06a5298efa | ||
|
|
db720acc18 | ||
|
|
8d8ff25bae | ||
|
|
6f85b7cc09 | ||
|
|
77f5770bff | ||
|
|
14cda5b921 | ||
|
|
36b1182565 | ||
|
|
483327c808 | ||
|
|
efb2f9a048 | ||
|
|
01978dabf0 | ||
|
|
cfbd2bf53a | ||
|
|
9262f94190 | ||
|
|
b36a0bd10b | ||
|
|
2dc8948f33 | ||
|
|
9f2ed2f9d4 | ||
|
|
e0f7d65f77 | ||
|
|
f18ac3a923 | ||
|
|
b39a6bcc97 | ||
|
|
8fa9c25f2a | ||
|
|
84bde62e05 | ||
|
|
5bb4621097 | ||
|
|
f1edf84f4d | ||
|
|
f7955a9040 | ||
|
|
7c5df21685 | ||
|
|
2060bb8cdd | ||
|
|
a9b4e7b1e2 | ||
|
|
82528c4478 | ||
|
|
4dcfc3e0bc | ||
|
|
999315d3c6 | ||
|
|
aef0b03c34 | ||
|
|
49f008c46f | ||
|
|
bd81db4117 | ||
|
|
9f274883e3 | ||
|
|
07b76b80f4 | ||
|
|
0014f0a88d | ||
|
|
63f7a78d31 | ||
|
|
e556c090ff | ||
|
|
c4f483c250 | ||
|
|
4031332b98 | ||
|
|
10cb7c8d6a | ||
|
|
be190bfe33 | ||
|
|
4d7d642952 | ||
|
|
737711e5eb | ||
|
|
42fc128f97 | ||
|
|
b03e6c3b19 | ||
|
|
66008489c4 | ||
|
|
3262e3490a | ||
|
|
16a73b0848 | ||
|
|
52bb4d7a0e | ||
|
|
40b5e4d549 | ||
|
|
b014ea8950 | ||
|
|
61592716f9 | ||
|
|
efe7fc0ee7 | ||
|
|
a810db3641 | ||
|
|
f8b292dfa3 | ||
|
|
fc2ab91280 | ||
|
|
668ee219c6 | ||
|
|
ee6512a1a6 | ||
|
|
fe1f590286 | ||
|
|
876e8f843a | ||
|
|
a45c1a1dc8 | ||
|
|
f8377169e6 | ||
|
|
a197a33d35 | ||
|
|
3060207d04 | ||
|
|
28c1d0b3f5 | ||
|
|
644d051477 | ||
|
|
f42031c8de | ||
|
|
6b50d9b087 | ||
|
|
a0f0da64b4 | ||
|
|
1e72ebd104 | ||
|
|
1184a95697 | ||
|
|
cd0e4a5678 | ||
|
|
394f6c3110 | ||
|
|
e2b6d0c256 | ||
|
|
fe7ca210dd | ||
|
|
e58d6bf2a3 | ||
|
|
460d20d6b2 | ||
|
|
ae154212fe | ||
|
|
28bb4edbac | ||
|
|
1ceed1b47b | ||
|
|
9445e64c2e | ||
|
|
e6fba03ba7 | ||
|
|
b027ca5c09 | ||
|
|
e98df30500 | ||
|
|
111c44dadf | ||
|
|
cb31c25e6c | ||
|
|
5d59c953c2 | ||
|
|
a2cff42981 | ||
|
|
cae892a971 | ||
|
|
f8447d923e | ||
|
|
3b8dabc9d2 | ||
|
|
cda39e967c | ||
|
|
7da9367dc9 | ||
|
|
82d97ef26f | ||
|
|
9e094f1d96 | ||
|
|
da7e15c268 | ||
|
|
24806a810c | ||
|
|
a2ace4510a | ||
|
|
5c8132088f | ||
|
|
7ee060b228 | ||
|
|
4b2a4b8f7b | ||
|
|
5a668ede01 | ||
|
|
9e099444b6 | ||
|
|
e3f5b3535a | ||
|
|
346ad1e8a3 | ||
|
|
2a9558e9c5 | ||
|
|
c324f66aef | ||
|
|
e688f2cdb6 | ||
|
|
2ff8762a22 | ||
|
|
4d75221938 | ||
|
|
658b699b71 | ||
|
|
72b0d03546 | ||
|
|
19980807f2 | ||
|
|
3514e3d057 | ||
|
|
fb6c82740c | ||
|
|
8df5a010c9 | ||
|
|
895e9b8bf0 | ||
|
|
bfcf85e0d2 | ||
|
|
5770222304 | ||
|
|
d5d2815bdf | ||
|
|
7fc3d413e5 | ||
|
|
474228b630 | ||
|
|
0805e077a1 | ||
|
|
6b059a9a75 | ||
|
|
7377e4e34d | ||
|
|
6fac947d9c | ||
|
|
5973e5ca26 | ||
|
|
608ab53933 | ||
|
|
05679f7a8d | ||
|
|
fcf51ec6da | ||
|
|
d15b5f8cbc | ||
|
|
ef3cbcac6d | ||
|
|
b16893c4d2 | ||
|
|
a365814aaa | ||
|
|
eca2116adc | ||
|
|
4cfa403657 | ||
|
|
70ded4c2cd | ||
|
|
3fe45db6ef | ||
|
|
11467775b6 | ||
|
|
1e5fcf635e | ||
|
|
223ba4643f | ||
|
|
200fd76929 | ||
|
|
c5c4ecd6e6 | ||
|
|
bedca9f82c | ||
|
|
adc356eff3 | ||
|
|
c4285772b3 | ||
|
|
a02235f2bd | ||
|
|
5f215b14c2 | ||
|
|
6e29fe8d58 | ||
|
|
43ceb1982d | ||
|
|
d02ec47d77 | ||
|
|
9942950710 |
8
.github/workflows/main-docker.yml
vendored
8
.github/workflows/main-docker.yml
vendored
@@ -86,12 +86,12 @@ jobs:
|
||||
|
||||
- name: Upload Playwright trace
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Playwright trace (${{ matrix.dockerfile }})
|
||||
path: test-output/playwright/output
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: Playwright report (${{ matrix.dockerfile }})
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
|
||||
path: /tmp/digests/*
|
||||
@@ -227,7 +227,7 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
|
||||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@@ -102,7 +102,7 @@ jobs:
|
||||
name: Nightly Build
|
||||
|
||||
- name: Publish artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
|
||||
|
||||
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
|
||||
- name: Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: e2e report ${{ matrix.arch }}
|
||||
path: apps/server-e2e/test-output
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Upload the artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
|
||||
path: apps/desktop/upload/*.*
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Upload the artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-server-linux-${{ matrix.arch }}
|
||||
path: upload/*.*
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
docs/Release Notes
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
merge-multiple: true
|
||||
pattern: release-*
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -44,9 +44,10 @@ upload
|
||||
.rollup.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
/.direnv
|
||||
/result
|
||||
.svelte-kit
|
||||
|
||||
# docs
|
||||
site/
|
||||
apps/*/coverage
|
||||
apps/*/coverage
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -37,6 +37,9 @@
|
||||
"apps/server/src/assets/doc_notes/**": true,
|
||||
"apps/edit-docs/demo/**": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "*", "severity": "warn" }
|
||||
]
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.24.0",
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.12.3",
|
||||
"@redocly/cli": "2.12.6",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.2",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"typedoc": "0.28.15",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "16.5.0",
|
||||
"i18next": "25.7.1",
|
||||
"i18next": "25.7.2",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -60,7 +60,7 @@
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.0",
|
||||
"react-i18next": "16.4.0",
|
||||
"react-i18next": "16.5.0",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
|
||||
@@ -265,7 +265,7 @@ export type CommandMappings = {
|
||||
|
||||
reEvaluateRightPaneVisibility: CommandData;
|
||||
runActiveNote: CommandData;
|
||||
scrollContainerToCommand: CommandData & {
|
||||
scrollContainerTo: CommandData & {
|
||||
position: number;
|
||||
};
|
||||
scrollToEnd: CommandData;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import Component from "./component.js";
|
||||
import froca from "../services/froca.js";
|
||||
import hoistedNoteService from "../services/hoisted_note.js";
|
||||
import options from "../services/options.js";
|
||||
import type { ViewScope } from "../services/link.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import { closeActiveDialog } from "../services/dialog.js";
|
||||
import froca from "../services/froca.js";
|
||||
import hoistedNoteService from "../services/hoisted_note.js";
|
||||
import type { ViewScope } from "../services/link.js";
|
||||
import options from "../services/options.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import server from "../services/server.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||
import appContext, { type EventData, type EventListener } from "./app_context.js";
|
||||
import Component from "./component.js";
|
||||
|
||||
export interface SetNoteOpts {
|
||||
triggerSwitchEvent?: unknown;
|
||||
|
||||
@@ -6,7 +6,7 @@ import openService from "../services/open.js";
|
||||
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 utils, { openInReusableSplit } from "../services/utils.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
|
||||
@@ -193,6 +193,16 @@ export default class RootCommandExecutor extends Component {
|
||||
appContext.triggerEvent("zenModeChanged", { isEnabled });
|
||||
}
|
||||
|
||||
async toggleRibbonTabNoteMapCommand() {
|
||||
const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js");
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
if (!isNewLayout) return;
|
||||
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (!activeContext?.notePath) return;
|
||||
openInReusableSplit(activeContext.notePath, "note-map");
|
||||
}
|
||||
|
||||
firstTabCommand() {
|
||||
this.#goToTab(1);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ function initOnElectron() {
|
||||
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||
initTitleBarButtons(style, currentWindow);
|
||||
}
|
||||
|
||||
// Clear navigation history on frontend refresh.
|
||||
currentWindow.webContents.navigationHistory.clear();
|
||||
}
|
||||
|
||||
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||
|
||||
@@ -1,49 +1,57 @@
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import type { AppContext } from "../components/app_context.js";
|
||||
import type { WidgetsByParent } from "../services/bundle.js";
|
||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
||||
import options from "../services/options.js";
|
||||
import utils from "../services/utils.js";
|
||||
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 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 NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.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 QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
import ContentHeader from "../widgets/containers/content_header.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||
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 SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
||||
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
||||
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
||||
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
|
||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
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 TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||
import TitleBarButtons from "../widgets/title_bar_buttons.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";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@@ -69,16 +77,31 @@ export default class DesktopLayout {
|
||||
*/
|
||||
const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac);
|
||||
const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows;
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
const titleRow = new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.optChild(isNewLayout, <NoteBadges />)
|
||||
.optChild(!isNewLayout, <SpacerWidget baseSize={0} growthFactor={1} />)
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
.child(<ClosePaneButton />)
|
||||
.child(<CreatePaneButton />)
|
||||
.optChild(isNewLayout, <NoteActions />);
|
||||
|
||||
const rootContainer = new RootContainer(true)
|
||||
.setParent(appContext)
|
||||
.class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout")
|
||||
.class(`${launcherPaneIsHorizontal ? "horizontal" : "vertical" }-layout`)
|
||||
.optChild(
|
||||
fullWidthTabBar,
|
||||
new FlexContainer("row")
|
||||
.class("tab-row-container")
|
||||
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget().class("full-width"))
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px")
|
||||
@@ -101,7 +124,13 @@ export default class DesktopLayout {
|
||||
new FlexContainer("column")
|
||||
.id("rest-pane")
|
||||
.css("flex-grow", "1")
|
||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
|
||||
.optChild(!fullWidthTabBar,
|
||||
new FlexContainer("row")
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget())
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px"))
|
||||
.optChild(isNewLayout, <FixedFormattingToolbar />)
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.filling()
|
||||
@@ -115,28 +144,17 @@ export default class DesktopLayout {
|
||||
.child(
|
||||
new SplitNoteContainer(() =>
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.class("title-row")
|
||||
.css("height", "50px")
|
||||
.css("min-height", "50px")
|
||||
.css("align-items", "center")
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
.child(<ClosePaneButton />)
|
||||
.child(<CreatePaneButton />)
|
||||
)
|
||||
.child(<Ribbon />)
|
||||
.child(titleRow)
|
||||
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||
.optChild(isNewLayout, <Ribbon />)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.child(new ContentHeader()
|
||||
.optChild(isNewLayout, <InlineTitle />)
|
||||
.optChild(isNewLayout, <NoteTitleActions />)
|
||||
.optChild(!isNewLayout, new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfo />)
|
||||
)
|
||||
@@ -157,6 +175,7 @@ export default class DesktopLayout {
|
||||
)
|
||||
)
|
||||
.child(...this.customWidgets.get("center-pane"))
|
||||
|
||||
)
|
||||
.child(
|
||||
new RightPaneContainer()
|
||||
@@ -165,8 +184,10 @@ export default class DesktopLayout {
|
||||
.child(...this.customWidgets.get("right-pane"))
|
||||
)
|
||||
)
|
||||
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||
)
|
||||
)
|
||||
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||
.child(<CloseZenModeButton />)
|
||||
|
||||
// Desktop-specific dialogs.
|
||||
|
||||
@@ -52,5 +52,5 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(<IncorrectCpuArchDialog />)
|
||||
.child(<PopupEditorDialog />)
|
||||
.child(<CallToActionDialog />)
|
||||
.child(<ToastContainer />)
|
||||
.child(<ToastContainer />);
|
||||
}
|
||||
|
||||
51
apps/client/src/services/experimental_features.ts
Normal file
51
apps/client/src/services/experimental_features.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { t } from "./i18n";
|
||||
import options from "./options";
|
||||
|
||||
export interface ExperimentalFeature {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const experimentalFeatures = [
|
||||
{
|
||||
id: "new-layout",
|
||||
name: t("experimental_features.new_layout_name"),
|
||||
description: t("experimental_features.new_layout_description"),
|
||||
}
|
||||
] as const satisfies ExperimentalFeature[];
|
||||
|
||||
export type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"];
|
||||
|
||||
let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
||||
|
||||
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
||||
return getEnabledFeatures().has(featureId);
|
||||
}
|
||||
|
||||
export function getEnabledExperimentalFeatureIds() {
|
||||
return getEnabledFeatures().values();
|
||||
}
|
||||
|
||||
export async function toggleExperimentalFeature(featureId: ExperimentalFeatureId, enable: boolean) {
|
||||
const features = new Set(getEnabledFeatures());
|
||||
if (enable) {
|
||||
features.add(featureId);
|
||||
} else {
|
||||
features.delete(featureId);
|
||||
}
|
||||
await options.save("experimentalFeatures", JSON.stringify(Array.from(features)));
|
||||
}
|
||||
|
||||
function getEnabledFeatures() {
|
||||
if (!enabledFeatures) {
|
||||
let features: ExperimentalFeatureId[] = [];
|
||||
try {
|
||||
features = JSON.parse(options.get("experimentalFeatures")) as ExperimentalFeatureId[];
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse experimental features from options:", e);
|
||||
}
|
||||
enabledFeatures = new Set(features);
|
||||
}
|
||||
return enabledFeatures;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
export type ViewMode = "default" | "source" | "attachments" | "contextual-help";
|
||||
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map";
|
||||
|
||||
export interface ViewScope {
|
||||
/**
|
||||
@@ -99,7 +99,7 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
|
||||
const viewMode = viewScope.viewMode || "default";
|
||||
let linkTitle = options.title;
|
||||
|
||||
if (!linkTitle) {
|
||||
if (linkTitle === undefined) {
|
||||
if (viewMode === "attachments" && viewScope.attachmentId) {
|
||||
const attachment = await froca.getAttachment(viewScope.attachmentId);
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
|
||||
export const NOTE_PATH_TITLE_SEPARATOR = " › ";
|
||||
|
||||
async function resolveNotePath(notePath: string, hoistedNoteId = "root") {
|
||||
const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId);
|
||||
|
||||
@@ -254,7 +256,7 @@ async function getNotePathTitle(notePath: string) {
|
||||
|
||||
const titlePath = await getNotePathTitleComponents(notePath);
|
||||
|
||||
return titlePath.join(" / ");
|
||||
return titlePath.join(NOTE_PATH_TITLE_SEPARATOR);
|
||||
}
|
||||
|
||||
async function getNoteTitleWithPathAsSuffix(notePath: string) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import type { ViewScope } from "./link.js";
|
||||
import type { ViewMode, ViewScope } from "./link.js";
|
||||
import FNote from "../entities/fnote";
|
||||
import { snapdom } from "@zumer/snapdom";
|
||||
|
||||
@@ -439,7 +439,20 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
|
||||
* @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
|
||||
* @returns a promise that resolves once the help has been opened.
|
||||
*/
|
||||
export async function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||
export function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||
return openInReusableSplit(`_help_${inAppHelpPage}`, "contextual-help");
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to opening a new note in a split, but re-uses an existing split if there is already one open with the same view mode.
|
||||
*
|
||||
* @param targetNoteId the note ID to open in the split.
|
||||
* @param targetViewMode the view mode of the split to open the note in.
|
||||
* @param openOpts additional options for opening the note.
|
||||
*/
|
||||
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
|
||||
hoistedNoteId?: string;
|
||||
} = {}) {
|
||||
// Dynamic import to avoid import issues in tests.
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
@@ -447,23 +460,20 @@ export async function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||
return;
|
||||
}
|
||||
const subContexts = activeContext.getSubContexts();
|
||||
const targetNote = `_help_${inAppHelpPage}`;
|
||||
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
|
||||
const viewScope: ViewScope = {
|
||||
viewMode: "contextual-help",
|
||||
};
|
||||
if (!helpSubcontext) {
|
||||
// The help is not already open, open a new split with it.
|
||||
const existingSubcontext = subContexts.find((s) => s.viewScope?.viewMode === targetViewMode);
|
||||
const viewScope: ViewScope = { viewMode: targetViewMode };
|
||||
if (!existingSubcontext) {
|
||||
// The target split is not already open, open a new split with it.
|
||||
const { ntxId } = subContexts[subContexts.length - 1];
|
||||
appContext.triggerCommand("openNewNoteSplit", {
|
||||
ntxId,
|
||||
notePath: targetNote,
|
||||
hoistedNoteId: "_help",
|
||||
notePath: targetNoteId,
|
||||
hoistedNoteId: openOpts.hoistedNoteId,
|
||||
viewScope
|
||||
})
|
||||
});
|
||||
} else {
|
||||
// There is already a help window open, make sure it opens on the right note.
|
||||
helpSubcontext.setNote(targetNote, { viewScope });
|
||||
// There is already a target split open, make sure it opens on the right note.
|
||||
existingSubcontext.setNote(targetNoteId, { viewScope });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
--bs-body-font-family: var(--main-font-family) !important;
|
||||
--bs-body-font-weight: var(--main-font-weight) !important;
|
||||
--bs-body-color: var(--main-text-color) !important;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--tn-modal-max-height: 90vh;
|
||||
|
||||
--tree-item-light-theme-max-color-lightness: 50;
|
||||
@@ -423,16 +423,16 @@ body.desktop .tabulator-popup-container,
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown-menu .disabled .disabled-tooltip {
|
||||
.dropdown-menu .disabled .contextual-help {
|
||||
pointer-events: all;
|
||||
margin-inline-start: 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--disabled-tooltip-icon-color);
|
||||
color: var(--contextual-help-icon-color);
|
||||
cursor: help;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.dropdown-menu .disabled .disabled-tooltip:hover {
|
||||
.dropdown-menu .disabled .contextual-help:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -471,7 +471,7 @@ body.mobile .dropdown .dropdown-submenu > span {
|
||||
padding-inline-start: 12px;
|
||||
}
|
||||
|
||||
.dropdown-menu kbd {
|
||||
.dropdown-menu kbd {
|
||||
color: var(--muted-text-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
@@ -487,7 +487,7 @@ body.mobile .dropdown .dropdown-submenu > span {
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
|
||||
/* This is a workaround for Firefox not supporting break-before / break-after: avoid on columns.
|
||||
* It usually wraps a menu item followed by a separator / header and another menu item. */
|
||||
.dropdown-no-break {
|
||||
break-inside: avoid;
|
||||
@@ -1315,12 +1315,18 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
|
||||
top: 0;
|
||||
inset-inline-start: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
|
||||
margin-top: -10px;
|
||||
min-width: 15rem;
|
||||
min-width: max-content;
|
||||
max-width: 300px;
|
||||
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dropdown-submenu.dropstart > .dropdown-menu {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: calc(100% - 2px);
|
||||
}
|
||||
|
||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: calc(-100% + 10px);
|
||||
}
|
||||
@@ -1367,6 +1373,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
background-color: var(--scrollbar-background-color);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: inherit;
|
||||
}
|
||||
@@ -1591,7 +1601,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
margin: 0 !important;
|
||||
max-height: 85vh;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -2093,7 +2103,7 @@ body.zen .note-split.type-text .scrolling-container {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -2365,7 +2375,7 @@ footer.webview-footer button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admonition::before {
|
||||
.admonition::before {
|
||||
color: var(--accent-color);
|
||||
font-family: boxicons !important;
|
||||
position: absolute;
|
||||
@@ -2391,7 +2401,7 @@ footer.webview-footer button {
|
||||
|
||||
.ck-content ul.todo-list li:has(> span.todo-list__label input[type="checkbox"]:checked) > span.todo-list__label span.todo-list__label__description {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.chat-options-container {
|
||||
@@ -2524,6 +2534,7 @@ iframe.print-iframe {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Calendar collection */
|
||||
@@ -2538,7 +2549,7 @@ iframe.print-iframe {
|
||||
body.mobile {
|
||||
.split-note-container-widget {
|
||||
flex-direction: column !important;
|
||||
|
||||
|
||||
.note-split {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -2553,4 +2564,10 @@ iframe.print-iframe {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.desktop .title-row {
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
--dropdown-border-color: #555;
|
||||
--dropdown-shadow-opacity: 0.4;
|
||||
--dropdown-item-icon-destructive-color: #de6e5b;
|
||||
--disabled-tooltip-icon-color: #7fd2ef;
|
||||
--contextual-help-icon-color: #7fd2ef;
|
||||
|
||||
--accented-background-color: #555;
|
||||
--more-accented-background-color: #777;
|
||||
@@ -115,7 +115,3 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
.use-note-color {
|
||||
--custom-color: var(--dark-theme-custom-color);
|
||||
}
|
||||
|
||||
span.fancytree-active {
|
||||
color: var(--dark-theme-custom-color);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ html {
|
||||
--dropdown-border-color: #ccc;
|
||||
--dropdown-shadow-opacity: 0.2;
|
||||
--dropdown-item-icon-destructive-color: #ec5138;
|
||||
--disabled-tooltip-icon-color: #004382;
|
||||
--contextual-help-icon-color: #004382;
|
||||
|
||||
--accented-background-color: #f5f5f5;
|
||||
--more-accented-background-color: #ddd;
|
||||
@@ -99,7 +99,3 @@ html {
|
||||
.use-note-color {
|
||||
--custom-color: var(--light-theme-custom-color);
|
||||
}
|
||||
|
||||
span.fancytree-active {
|
||||
color: var(--light-theme-custom-color);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
:root {
|
||||
|
||||
/*
|
||||
/*
|
||||
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
|
||||
* The names and purposes of these CSS variables are subject to frequent changes.
|
||||
*/
|
||||
@@ -22,7 +22,7 @@
|
||||
--dropdown-border-color: #404040;
|
||||
--dropdown-shadow-opacity: 0.6;
|
||||
--dropdown-item-icon-destructive-color: #de6e5b;
|
||||
--disabled-tooltip-icon-color: #7fd2ef;
|
||||
--contextual-help-icon-color: #7fd2ef;
|
||||
|
||||
--accented-background-color: #555;
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
|
||||
--tab-close-button-hover-background: #a45353;
|
||||
--tab-close-button-hover-color: white;
|
||||
|
||||
|
||||
--active-tab-background-color: #ffffff1c;
|
||||
--active-tab-hover-background-color: var(--active-tab-background-color);
|
||||
--active-tab-icon-color: #a9a9a9;
|
||||
@@ -201,7 +201,7 @@
|
||||
|
||||
--promoted-attribute-card-background-color: #ffffff21;
|
||||
--promoted-attribute-card-shadow: none;
|
||||
|
||||
|
||||
--floating-button-shadow-color: #00000080;
|
||||
--floating-button-background-color: #494949d2;
|
||||
--floating-button-color: var(--button-text-color);
|
||||
@@ -226,7 +226,7 @@
|
||||
--scrollbar-border-color: unset; /* Deprecated */
|
||||
|
||||
--selection-background-color: #3399FF70;
|
||||
|
||||
|
||||
--link-color: lightskyblue;
|
||||
|
||||
--mermaid-theme: dark;
|
||||
@@ -320,4 +320,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
|
||||
.use-note-color {
|
||||
--custom-color: var(--dark-theme-custom-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
:root {
|
||||
|
||||
/*
|
||||
/*
|
||||
* ⚠️ NOTICE: This theme is currently in the beta stage of development.
|
||||
* The names and purposes of these CSS variables are subject to frequent changes.
|
||||
*/
|
||||
@@ -22,7 +22,7 @@
|
||||
--dropdown-border-color: #ccc;
|
||||
--dropdown-shadow-opacity: 0.2;
|
||||
--dropdown-item-icon-destructive-color: #ec5138;
|
||||
--disabled-tooltip-icon-color: #004382;
|
||||
--contextual-help-icon-color: #004382;
|
||||
|
||||
--accented-background-color: #f5f5f5;
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
|
||||
--launcher-pane-background-color: unset;
|
||||
--launcher-pane-text-color: unset;
|
||||
|
||||
|
||||
--launcher-pane-vert-background-color: #e8e8e8;
|
||||
--launcher-pane-vert-text-color: #000000bd;
|
||||
--launcher-pane-vert-button-hover-color: black;
|
||||
@@ -174,7 +174,7 @@
|
||||
|
||||
--tab-close-button-hover-background: #c95a5a;
|
||||
--tab-close-button-hover-color: white;
|
||||
|
||||
|
||||
--active-tab-background-color: white;
|
||||
--active-tab-hover-background-color: var(--active-tab-background-color);
|
||||
--active-tab-icon-color: gray;
|
||||
@@ -291,4 +291,4 @@
|
||||
--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%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,13 +89,13 @@
|
||||
* the color is adjusted based on the current color scheme (light or dark). The lightness
|
||||
* component of the color represented in the CIELAB color space, will be
|
||||
* constrained to a certain percentage defined below.
|
||||
*
|
||||
*
|
||||
* Note: the tree background may vary when background effects are enabled, so it is recommended
|
||||
* to maintain a higher contrast margin than on the usual note tree solid background. */
|
||||
|
||||
/* The maximum perceptual lightness for the custom color in the light theme (%): */
|
||||
--tree-item-light-theme-max-color-lightness: 60;
|
||||
|
||||
|
||||
/* The minimum perceptual lightness for the custom color in the dark theme (%): */
|
||||
--tree-item-dark-theme-min-color-lightness: 65;
|
||||
}
|
||||
@@ -165,7 +165,7 @@ body.desktop .dropdown-submenu .dropdown-menu {
|
||||
--menu-item-start-padding: 8px;
|
||||
--menu-item-end-padding: 22px;
|
||||
--menu-item-vertical-padding: 2px;
|
||||
|
||||
|
||||
padding-top: var(--menu-item-vertical-padding) !important;
|
||||
padding-bottom: var(--menu-item-vertical-padding) !important;
|
||||
padding-inline-start: var(--menu-item-start-padding) !important;
|
||||
@@ -176,6 +176,11 @@ body.desktop .dropdown-submenu .dropdown-menu {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item {
|
||||
padding-inline-end: var(--menu-item-start-padding) !important;
|
||||
padding-inline-start: var(--menu-item-end-padding) !important;
|
||||
}
|
||||
|
||||
:root .dropdown-item:focus-visible {
|
||||
outline: 2px solid var(--input-focus-outline-color) !important;
|
||||
background-color: transparent;
|
||||
@@ -249,7 +254,7 @@ html body .dropdown-item[disabled] {
|
||||
}
|
||||
|
||||
/* Menu item arrow */
|
||||
.dropdown-menu .dropdown-toggle::after {
|
||||
.dropdown-submenu:not(.dropstart) .dropdown-toggle::after {
|
||||
content: "\ed3b" !important;
|
||||
position: absolute;
|
||||
display: flex !important;
|
||||
@@ -265,6 +270,22 @@ html body .dropdown-item[disabled] {
|
||||
color: var(--menu-item-arrow-color) !important;
|
||||
}
|
||||
|
||||
.dropdown-submenu.dropstart .dropdown-toggle::before {
|
||||
content: "\ea4d" !important;
|
||||
position: absolute;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
margin: unset !important;
|
||||
border: unset !important;
|
||||
padding: 0 4px;
|
||||
font-family: boxicons;
|
||||
font-size: 1.2em;
|
||||
color: var(--menu-item-arrow-color) !important;
|
||||
}
|
||||
|
||||
body[dir=rtl] .dropdown-menu:not([data-popper-placement="bottom-start"]) .dropdown-toggle::after {
|
||||
content: "\ea4d" !important;
|
||||
}
|
||||
@@ -339,7 +360,7 @@ body.mobile .dropdown-menu {
|
||||
font-size: 1em !important;
|
||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||
position: relative;
|
||||
|
||||
|
||||
.dropdown-toggle::after {
|
||||
top: 0.5em;
|
||||
right: var(--dropdown-menu-padding-horizontal);
|
||||
@@ -356,7 +377,7 @@ body.mobile .dropdown-menu {
|
||||
padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal) !important;
|
||||
background: var(--card-background-color);
|
||||
border-bottom: 1px solid var(--menu-item-delimiter-color) !important;
|
||||
border-radius: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.dropdown-item:first-of-type,
|
||||
@@ -367,9 +388,9 @@ body.mobile .dropdown-menu {
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.dropdown-item:last-of-type,
|
||||
.dropdown-item:last-of-type,
|
||||
.dropdown-item:has(+ .dropdown-divider),
|
||||
.dropdown-custom-item:last-of-type,
|
||||
.dropdown-custom-item:last-of-type,
|
||||
.dropdown-custom-item:has(+ .dropdown-divider) {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
@@ -392,10 +413,10 @@ body.mobile .dropdown-menu {
|
||||
--menu-background-color: --menu-submenu-mobile-background-color;
|
||||
--bs-dropdown-divider-margin-y: 0.25rem;
|
||||
border-radius: 0;
|
||||
max-height: 0;
|
||||
max-height: 0;
|
||||
transition: max-height 100ms ease-in;
|
||||
display: block !important;
|
||||
|
||||
display: block !important;
|
||||
|
||||
&.show {
|
||||
max-height: 1000px;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
@@ -405,7 +426,7 @@ body.mobile .dropdown-menu {
|
||||
&.submenu-open {
|
||||
.dropdown-toggle {
|
||||
padding-bottom: var(--dropdown-menu-padding-vertical);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,4 +764,4 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
.note-detail-empty .aa-suggestions div.aa-cursor {
|
||||
background: var(--hover-item-background-color);
|
||||
color: var(--hover-item-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ button.btn.btn-success kbd {
|
||||
color: var(--button-group-active-button-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Input boxes
|
||||
*/
|
||||
|
||||
@@ -399,7 +399,8 @@ button.select-button.dropdown-toggle.btn:active {
|
||||
select:focus,
|
||||
select.form-select:focus,
|
||||
select.form-control:focus,
|
||||
.select-button.dropdown-toggle.btn:focus {
|
||||
.select-button.dropdown-toggle.btn:focus,
|
||||
.select-button.focus-outline:focus {
|
||||
box-shadow: unset;
|
||||
outline: 3px solid var(--input-focus-outline-color);
|
||||
outline-offset: 0;
|
||||
@@ -422,7 +423,7 @@ optgroup {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* File input
|
||||
*
|
||||
* <label class="tn-file-input tn-input-field">
|
||||
@@ -784,4 +785,4 @@ input[type="range"] {
|
||||
scrollbar-color: unset;
|
||||
scrollbar-width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ ul.editability-dropdown li.dropdown-item > div {
|
||||
background: var(--cmd-button-hover-background-color);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Note info
|
||||
*/
|
||||
|
||||
@@ -177,4 +177,4 @@ ul.editability-dropdown li.dropdown-item > div {
|
||||
/* Narrow width layout */
|
||||
.note-info-widget {
|
||||
container: info-section / inline-size;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +502,7 @@ div.bookmark-folder-widget .note-link .bx {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* QUICK SEARCH BOX
|
||||
*/
|
||||
|
||||
@@ -613,7 +613,7 @@ div.quick-search .dropdown-menu {
|
||||
* As a temporary workaround, the backdrop and transparency are disabled for the
|
||||
* vertical layout.
|
||||
*/
|
||||
body.layout-vertical.background-effects div.quick-search .dropdown-menu {
|
||||
body.layout-vertical.background-effects div.quick-search .dropdown-menu {
|
||||
--menu-background-color: var(--menu-background-color-no-backdrop) !important;
|
||||
}
|
||||
|
||||
@@ -945,12 +945,26 @@ body.electron.background-effects.layout-horizontal .tab-row-container .toggle-bu
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: -10px;
|
||||
inset-inline-end: -10px;
|
||||
inset-inline-end: -6px;
|
||||
top: 32px;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-history-navigation-buttons {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: -7px;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
|
||||
position: relative;
|
||||
@@ -1569,7 +1583,7 @@ div.floating-buttons .show-floating-buttons-button {
|
||||
div.floating-buttons .show-floating-buttons-button::before {
|
||||
animation: floating-buttons-show-hide-button-animation 400ms ease-out;
|
||||
}
|
||||
|
||||
|
||||
div.floating-buttons .show-floating-buttons-button:hover,
|
||||
div.floating-buttons .show-floating-buttons-button:active {
|
||||
box-shadow: var(--floating-button-show-button-hover-shadow);
|
||||
@@ -1831,7 +1845,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
||||
|
||||
.excalidraw .dropdown-menu {
|
||||
border: unset !important;
|
||||
box-shadow: unset !important;
|
||||
box-shadow: unset !important;
|
||||
background-color: transparent !important;
|
||||
--island-bg-color: var(--menu-background-color);
|
||||
--shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
@@ -1850,4 +1864,4 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
||||
|
||||
.excalidraw .dropdown-menu:before {
|
||||
content: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import utils from "../services/utils.js";
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
import FBlob from "../entities/fblob.js";
|
||||
import FBranch from "../entities/fbranch.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import froca from "../services/froca.js";
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||
import FBranch from "../entities/fbranch.js";
|
||||
import FBlob from "../entities/fblob.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
type AttributeDefinitions = { [key in `#${string}`]: string; };
|
||||
type RelationDefinitions = { [key in `~${string}`]: string; };
|
||||
@@ -12,6 +14,7 @@ type RelationDefinitions = { [key in `~${string}`]: string; };
|
||||
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
|
||||
id?: string | undefined;
|
||||
title: string;
|
||||
type?: NoteType;
|
||||
children?: NoteDefinition[];
|
||||
content?: string;
|
||||
}
|
||||
@@ -45,7 +48,7 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
const note = new FNote(froca, {
|
||||
noteId: noteDef.id ?? utils.randomString(12),
|
||||
title: noteDef.title,
|
||||
type: "text",
|
||||
type: noteDef.type ?? "text",
|
||||
mime: "text/html",
|
||||
isProtected: false,
|
||||
blobId: ""
|
||||
|
||||
@@ -693,7 +693,10 @@
|
||||
"convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
|
||||
"convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?",
|
||||
"print_pdf": "导出为 PDF...",
|
||||
"open_note_on_server": "在服务器上打开笔记"
|
||||
"open_note_on_server": "在服务器上打开笔记",
|
||||
"view_revisions": "笔记修订...",
|
||||
"note_map": "笔记地图",
|
||||
"advanced": "高级"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
|
||||
@@ -1577,7 +1580,13 @@
|
||||
"printing_pdf": "正在导出为PDF…"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "请输入笔记标题..."
|
||||
"placeholder": "请输入笔记标题...",
|
||||
"created_on": "建立于 <Value />",
|
||||
"last_modified": "最后修改于 <Value />",
|
||||
"note_type_switcher_label": "从 {{type}} 切换到:",
|
||||
"note_type_switcher_others": "更多笔记类型",
|
||||
"note_type_switcher_templates": "模板",
|
||||
"note_type_switcher_collection": "集合"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "没有找到符合搜索条件的笔记。",
|
||||
@@ -1939,8 +1948,9 @@
|
||||
"unknown_widget": "未知组件:\"{{id}}\"."
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "不设置",
|
||||
"configure-languages": "设置语言..."
|
||||
"not_set": "未设置语言",
|
||||
"configure-languages": "设置语言...",
|
||||
"help-on-languages": "内容语言帮助..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "内容语言",
|
||||
@@ -2007,7 +2017,7 @@
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "隐藏周末",
|
||||
"display-week-numbers": "显示周数",
|
||||
"map-style": "地图样式:",
|
||||
"map-style": "地图样式",
|
||||
"max-nesting-depth": "最大嵌套深度:",
|
||||
"raster": "栅格",
|
||||
"vector_light": "矢量(浅色)",
|
||||
@@ -2116,5 +2126,43 @@
|
||||
"unknown_http_error_title": "与服务器通讯错误",
|
||||
"unknown_http_error_content": "状态码: {{statusCode}}\n地址: {{method}} {{url}}\n信息: {{message}}",
|
||||
"traefik_blocks_requests": "如果您使用 Traefik 反向代理,它引入了一项影响与服务器的通信重大更改。"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "实验选项",
|
||||
"disclaimer": "这些选项处于实验阶段,可能导致系统不稳定。请谨慎使用。",
|
||||
"new_layout_name": "新布局",
|
||||
"new_layout_description": "尝试全新布局,呈现更现代的外观并提升易用性。后续版本将进行重大调整。"
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "返回前一笔记",
|
||||
"go-forward": "前往下一笔记"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "只读",
|
||||
"read_only_auto": "自动只读",
|
||||
"shared_publicly": "公开共享",
|
||||
"shared_locally": "本地共享",
|
||||
"read_only_explicit_description": "此笔记已被手动设置为只读。\n点击可临时编辑。",
|
||||
"read_only_auto_description": "出于性能原因,此笔记已被自动设置为只读模式。此自动限制可以在设置中调整。\n\n点击可临时编辑。",
|
||||
"read_only_temporarily_disabled": "临时编辑",
|
||||
"read_only_temporarily_disabled_description": "此笔记当前可编辑,但通常是只读的。一旦你切换到其他笔记,该笔记将恢复为只读模式。\n\n点击以重新启用只读模式。",
|
||||
"shared_publicly_description": "此笔记已在网上发布,链接为 {{- link}},并且可公开访问。\n\n点击以导航到共享笔记,或右键点击查看更多选项。",
|
||||
"shared_locally_description": "此笔记仅在本地网络共享,链接为 {{- link}}。\n\n点击以导航到共享笔记,或右键点击查看更多选项。",
|
||||
"clipped_note": "网页剪辑",
|
||||
"clipped_note_description": "此笔记最初来自 {{url}}。\n\n点击即可跳转至源网页。",
|
||||
"execute_script": "运行脚本",
|
||||
"execute_script_description": "这是一篇脚本笔记。点击即可执行脚本。",
|
||||
"execute_sql": "运行SQL",
|
||||
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。"
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "更改所有内容的语言",
|
||||
"note_info_title": "查看有关此笔记的信息,例如创建/修改日期或笔记大小。",
|
||||
"backlinks_title_other": "此笔记由 {{count}} 个其他笔记链接而来。\n\n点击查看反向链接列表。",
|
||||
"attachments_title_other": "此笔记包含 {{count}} 个附件。点击即可在新标签页中打开附件列表。",
|
||||
"attributes_other": "{{count}} 个属性",
|
||||
"attributes_title": "单击以打开专用窗格,编辑此笔记拥有的属性,以及查看继承属性列表。",
|
||||
"note_paths_title": "点击查看此笔记在树状图中的位置路径。",
|
||||
"code_note_switcher": "更改语言模式"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,11 @@
|
||||
"cloned_note_prefix_title": "Klonovaná poznámka se zobrazí ve stromu poznámek s danou předponou",
|
||||
"clone_to_selected_note": "Klonovat vybranou poznámku",
|
||||
"no_path_to_clone_to": "Žádná cest pro klonování.",
|
||||
"note_cloned": "Poznámka: „{{clonedTitle}}“ bylo naklonováno do „{{targetTitle}}“"
|
||||
"note_cloned": "Poznámka „{{clonedTitle}}“ bylo naklonována do „{{targetTitle}}“"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink_one": "{{count}} zpětný odkaz",
|
||||
"backlink_few": "{{count}} zpětné odkazy",
|
||||
"backlink_other": "{{count}} zpětných odkazů"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,11 +689,14 @@
|
||||
"export_note": "Export note",
|
||||
"delete_note": "Delete note",
|
||||
"print_note": "Print note",
|
||||
"view_revisions": "Note revisions...",
|
||||
"save_revision": "Save revision",
|
||||
"advanced": "Advanced",
|
||||
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
|
||||
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
|
||||
"convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?",
|
||||
"print_pdf": "Export as PDF..."
|
||||
"print_pdf": "Export as PDF...",
|
||||
"note_map": "Note map"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Button widget '{{componentId}}' has no defined click handler"
|
||||
@@ -1096,6 +1099,12 @@
|
||||
"vacuuming_database": "Vacuuming database...",
|
||||
"database_vacuumed": "Database has been vacuumed"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "Experimental Options",
|
||||
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
|
||||
"new_layout_name": "New Layout",
|
||||
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases."
|
||||
},
|
||||
"fonts": {
|
||||
"theme_defined": "Theme defined",
|
||||
"fonts": "Fonts",
|
||||
@@ -1743,7 +1752,13 @@
|
||||
"printing_pdf": "Exporting to PDF in progress..."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "type note's title here..."
|
||||
"placeholder": "type note's title here...",
|
||||
"created_on": "Created on <Value />",
|
||||
"last_modified": "Last modified on <Value />",
|
||||
"note_type_switcher_label": "Switch from {{type}} to:",
|
||||
"note_type_switcher_others": "More note types",
|
||||
"note_type_switcher_templates": "Templates",
|
||||
"note_type_switcher_collection": "Collections"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "No notes have been found for given search parameters.",
|
||||
@@ -1953,8 +1968,9 @@
|
||||
"unknown_widget": "Unknown widget for \"{{id}}\"."
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "Not set",
|
||||
"configure-languages": "Configure languages..."
|
||||
"not_set": "No language set",
|
||||
"configure-languages": "Configure languages...",
|
||||
"help-on-languages": "Help on content languages..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "Content languages",
|
||||
@@ -2021,7 +2037,7 @@
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "Hide weekends",
|
||||
"display-week-numbers": "Display week numbers",
|
||||
"map-style": "Map style:",
|
||||
"map-style": "Map style",
|
||||
"max-nesting-depth": "Max nesting depth:",
|
||||
"raster": "Raster",
|
||||
"vector_light": "Vector (Light)",
|
||||
@@ -2117,5 +2133,40 @@
|
||||
"unknown_http_error_title": "Communication error with the server",
|
||||
"unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
|
||||
"traefik_blocks_requests": "If you are using the Traefik reverse proxy, it introduced a breaking change which affects the communication with the server."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Go back to previous note",
|
||||
"go-forward": "Go forward to next note"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Read-only",
|
||||
"read_only_explicit_description": "This note has been manually set to read-only.\nClick to edit it temporarily.",
|
||||
"read_only_auto": "Auto read-only",
|
||||
"read_only_auto_description": "This note was set automatically to read-only mode for performance reasons. This automatic limit is adjustable from settings.\n\nClick to edit it temporarily.",
|
||||
"read_only_temporarily_disabled": "Temporarily editable",
|
||||
"read_only_temporarily_disabled_description": "This note is currently editable, but it is normally read-only. The note will go back to being read-only as soon as you navigate to another note.\n\nClick to re-enable read-only mode.",
|
||||
"shared_publicly": "Shared publicly",
|
||||
"shared_publicly_description": "This note has been published online at {{- link}}, and is publicly accessible.\n\nClick to navigate to the shared note or right click for more options.",
|
||||
"shared_locally": "Shared locally",
|
||||
"shared_locally_description": "This note is shared on the local network only at {{- link}}.\n\nClick to navigate to the shared note or right click for more options.",
|
||||
"clipped_note": "Web clip",
|
||||
"clipped_note_description": "This note was originally taken from {{url}}.\n\nClick to navigate to the source webpage.",
|
||||
"execute_script": "Run script",
|
||||
"execute_script_description": "This note is a script note. Click to execute the script.",
|
||||
"execute_sql": "Run SQL",
|
||||
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Change the language of the entire content",
|
||||
"note_info_title": "View information about this note such as the creation/modification date or the note size.",
|
||||
"backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.",
|
||||
"backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.",
|
||||
"attachments_title_one": "This note has {{count}} attachment. Click to open the list of attachments in a new tab.",
|
||||
"attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab.",
|
||||
"attributes_one": "{{count}} attribute",
|
||||
"attributes_other": "{{count}} attributes",
|
||||
"attributes_title": "Click to open a dedicated pane to edit this note's owned attributes, as well as to see the list of inherited attributes.",
|
||||
"note_paths_title": "Click to see the paths where this note is placed into the tree.",
|
||||
"code_note_switcher": "Change language mode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@
|
||||
"info": {
|
||||
"okButton": "OK",
|
||||
"closeButton": "Chiudi",
|
||||
"modalTitle": "Messaggio informativo"
|
||||
"modalTitle": "Messaggio informativo",
|
||||
"copy_to_clipboard": "Copia negli appunti"
|
||||
},
|
||||
"export": {
|
||||
"close": "Chiudi",
|
||||
@@ -314,7 +315,7 @@
|
||||
"import-into-note": "Importa nella nota",
|
||||
"apply-bulk-actions": "Applica azioni in blocco",
|
||||
"converted-to-attachments": "{{count}} note sono state convertite in allegati.",
|
||||
"convert-to-attachment-confirm": "Sei sicuro di voler convertire le note selezionate in allegati delle note padre?",
|
||||
"convert-to-attachment-confirm": "Sei sicuro di voler convertire le note selezionate in allegati delle note principali? Questa operazione si applica solo alle note immagine, le altre note verranno ignorate.",
|
||||
"open-in-popup": "Modifica rapida"
|
||||
},
|
||||
"electron_context_menu": {
|
||||
@@ -1260,7 +1261,8 @@
|
||||
"convert_into_attachment_successful": "Nota '{{title}}' è stato convertito in allegato.",
|
||||
"convert_into_attachment_prompt": "Sei sicuro di voler convertire la nota '{{title}}' in un allegato della nota padre?",
|
||||
"print_pdf": "Esporta come PDF...",
|
||||
"open_note_on_server": "Apri una nota sul server"
|
||||
"open_note_on_server": "Apri una nota sul server",
|
||||
"view_revisions": "Revisioni..."
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Il widget pulsante '{{componentId}}' non ha un gestore di clic definito"
|
||||
@@ -1493,7 +1495,12 @@
|
||||
"editable_text": {
|
||||
"placeholder": "Digita qui il contenuto della tua nota...",
|
||||
"auto-detect-language": "Rilevato automaticamente",
|
||||
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
|
||||
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug.",
|
||||
"editor_crashed_title": "L'editor di testo si è bloccato",
|
||||
"editor_crashed_content": "I tuoi contenuti sono stati recuperati con successo, ma alcune delle modifiche più recenti potrebbero non essere state salvate.",
|
||||
"editor_crashed_details_button": "Visualizza ulteriori dettagli...",
|
||||
"editor_crashed_details_intro": "Se questo errore si verifica più volte, valuta la possibilità di segnalarlo su GitHub incollando le informazioni riportate di seguito.",
|
||||
"editor_crashed_details_title": "Informazioni tecniche"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Apri una nota digitandone il titolo nel campo sottostante oppure scegli una nota nell'albero.",
|
||||
@@ -1867,7 +1874,9 @@
|
||||
"printing_pdf": "Esportazione in PDF in corso..."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "scrivi qui il titolo della nota..."
|
||||
"placeholder": "scrivi qui il titolo della nota...",
|
||||
"created_on": "Creato il <Value />",
|
||||
"last_modified": "Ultima modifica il <Value />"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",
|
||||
@@ -2003,8 +2012,9 @@
|
||||
"unknown_widget": "Widget sconosciuto per \"{{id}}\"."
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "Non impostato",
|
||||
"configure-languages": "Configura le lingue..."
|
||||
"not_set": "Nessuna lingua impostata",
|
||||
"configure-languages": "Configura le lingue...",
|
||||
"help-on-languages": "Aiuto sulle lingue dei contenuti..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "Lingue dei contenuti",
|
||||
@@ -2022,7 +2032,8 @@
|
||||
"button_title": "Esporta diagramma come PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "Non è stato possibile esportare il diagramma in formato PNG."
|
||||
"export_to_png": "Non è stato possibile esportare il diagramma in formato PNG.",
|
||||
"export_to_svg": "Il diagramma non può essere esportato in formato SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Aspetto",
|
||||
@@ -2106,5 +2117,32 @@
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Passa all'editor completo"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "Opzioni sperimentali",
|
||||
"disclaimer": "Queste opzioni sono sperimentali e potrebbero causare instabilità. Usare con cautela.",
|
||||
"new_layout_name": "Nuovo layout",
|
||||
"new_layout_description": "Prova il nuovo layout per un look più moderno e una maggiore usabilità. Soggetto a modifiche significative nelle prossime versioni."
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "Errore di comunicazione con il server",
|
||||
"unknown_http_error_content": "Codice di stato: {{statusCode}}\nURL: {{method}} {{url}}\nMessaggio: {{message}}",
|
||||
"traefik_blocks_requests": "Se si utilizza il proxy inverso Traefik, è stata introdotta una modifica sostanziale che influisce sulla comunicazione con il server."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Torna alla nota precedente",
|
||||
"go-forward": "Passa alla nota successiva"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Sola lettura",
|
||||
"read_only_explicit_description": "Questa nota è stata impostata manualmente come di sola lettura.\nClicca per modificarla temporaneamente.",
|
||||
"read_only_auto": "Solo lettura automatica",
|
||||
"read_only_auto_description": "Questa nota è stata impostata automaticamente in modalità di sola lettura per motivi di prestazioni. Questo limite automatico è modificabile dalle impostazioni.\n\nClicca per modificarla temporaneamente.",
|
||||
"read_only_temporarily_disabled": "Modificabile temporaneamente",
|
||||
"read_only_temporarily_disabled_description": "Questa nota è attualmente modificabile, ma normalmente è di sola lettura. La nota tornerà ad essere di sola lettura non appena passerai a un'altra nota.\n\nClicca per riattivare la modalità di sola lettura.",
|
||||
"shared_publicly": "Condiviso pubblicamente",
|
||||
"shared_publicly_description": "Questa nota è stata pubblicata online all'indirizzo {{- link}} ed è accessibile al pubblico.\n\nClicca per visualizzare la nota condivisa o clicca con il tasto destro del mouse per ulteriori opzioni.",
|
||||
"shared_locally": "Condiviso localmente",
|
||||
"shared_locally_description": "Questa nota è condivisa sulla rete locale solo all'indirizzo {{- link}}.\n\nClicca per accedere alla nota condivisa o clicca con il tasto destro del mouse per ulteriori opzioni."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
"export_in_progress": "エクスポート処理中: {{progressCount}}",
|
||||
"export_finished_successfully": "エクスポートが正常に完了しました。",
|
||||
"format_pdf": "PDF - 印刷または共有目的に。",
|
||||
"share-format": "Web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 Web サイトとして公開できます。"
|
||||
"share-format": "web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 web サイトとして公開できます。"
|
||||
},
|
||||
"help": {
|
||||
"title": "チートシート",
|
||||
@@ -458,7 +458,10 @@
|
||||
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
|
||||
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
|
||||
"note_attachments": "ノートの添付ファイル",
|
||||
"open_note_on_server": "サーバー上のノートを開く"
|
||||
"open_note_on_server": "サーバー上のノートを開く",
|
||||
"view_revisions": "ノートの変更履歴...",
|
||||
"note_map": "ノートマップ",
|
||||
"advanced": "高度"
|
||||
},
|
||||
"command_palette": {
|
||||
"export_note_title": "ノートをエクスポート",
|
||||
@@ -799,7 +802,7 @@
|
||||
},
|
||||
"web_view": {
|
||||
"web_view": "Web ビュー",
|
||||
"embed_websites": "Web ビュータイプでは、ウェブサイトをTriliumに埋め込むことができます。",
|
||||
"embed_websites": "Web ビュータイプでは、web サイトを Trilium に埋め込むことができます。",
|
||||
"create_label": "まず始めに、埋め込みたいURLアドレスのラベルを作成してください。例: #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
@@ -961,7 +964,7 @@
|
||||
"password": {
|
||||
"wiki": "wiki",
|
||||
"heading": "パスワード",
|
||||
"alert_message": "新しいパスワードは大切に保管してください。パスワードはウェブインターフェースへのログインや、保護されたノートの暗号化に使用されます。パスワードを忘れると、保護されたノートはすべて永久に失われます。",
|
||||
"alert_message": "新しいパスワードは大切に保管してください。パスワードは web インターフェースへのログインや、保護されたノートの暗号化に使用されます。パスワードを忘れると、保護されたノートはすべて永久に失われます。",
|
||||
"reset_link": "リセットするにはここをクリック。",
|
||||
"old_password": "旧パスワード",
|
||||
"new_password": "新パスワード",
|
||||
@@ -1107,7 +1110,7 @@
|
||||
"sql_console_home": "SQLコンソールノートのデフォルトの場所",
|
||||
"bookmark_folder": "このラベルの付いたノートは、ブックマークにフォルダとして表示されます(子フォルダへのアクセスを許可します)",
|
||||
"share_hidden_from_tree": "このノートは左側のナビゲーションツリーには表示されていませんが、URL からアクセスできます",
|
||||
"share_external_link": "ノートは共有ツリー内で外部ウェブサイトへのリンクとして機能します",
|
||||
"share_external_link": "ノートは共有ツリー内で外部 web サイトへのリンクとして機能します",
|
||||
"share_alias": "https://your_trilium_host/share/[your_alias] でノートを利用できるようにエイリアスを定義します",
|
||||
"share_omit_default_css": "デフォルトの共有ページのCSSは省略されます。スタイルを大幅に変更する場合に使用してください。",
|
||||
"share_root": "/share root で提供されるノートをマークする。",
|
||||
@@ -1233,7 +1236,13 @@
|
||||
"none_yet": "アクションを上のリストからクリックして追加。"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "ここにノートのタイトルを入力..."
|
||||
"placeholder": "ここにノートのタイトルを入力...",
|
||||
"created_on": "作成日 <Value />",
|
||||
"last_modified": "最終更新日 <Value />",
|
||||
"note_type_switcher_label": "{{type}} から切り替え:",
|
||||
"note_type_switcher_others": "その他のノートタイプ",
|
||||
"note_type_switcher_templates": "テンプレート",
|
||||
"note_type_switcher_collection": "コレクション"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "指定された検索パラメータに該当するノートは見つかりませんでした。",
|
||||
@@ -1330,8 +1339,9 @@
|
||||
"minimum_input": "入力された時間値は {{minimumSeconds}} 秒以上である必要があります。"
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "未設定",
|
||||
"configure-languages": "言語を設定..."
|
||||
"not_set": "言語が設定されていません",
|
||||
"configure-languages": "言語を設定...",
|
||||
"help-on-languages": "コンテンツの言語に関するヘルプ..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "コンテンツの言語",
|
||||
@@ -1620,7 +1630,7 @@
|
||||
"remove_this_attribute": "この属性を削除",
|
||||
"remove_color": "このカラーラベルを削除",
|
||||
"promoted_attributes": "プロモート属性",
|
||||
"url_placeholder": "http://ウェブサイト..."
|
||||
"url_placeholder": "http://web サイト..."
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "新しいタブで開く",
|
||||
@@ -1974,7 +1984,7 @@
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "週末を非表示",
|
||||
"display-week-numbers": "週番号を表示",
|
||||
"map-style": "マップスタイル:",
|
||||
"map-style": "マップスタイル",
|
||||
"max-nesting-depth": "最大階層の深さ:",
|
||||
"show-scale": "スケールを表示",
|
||||
"raster": "Raster",
|
||||
@@ -2069,7 +2079,7 @@
|
||||
"recovery_keys_used": "使用日: {{date}}",
|
||||
"recovery_keys_unused": "回復コード {{index}} は未使用です",
|
||||
"oauth_title": "OAuth/OpenID",
|
||||
"oauth_description": "OpenIDは、Googleなどの他のサービスのアカウントを使用してウェブサイトにログインし、本人確認を行うための標準化された方法です。デフォルトの発行者はGoogleですが、他のOpenIDプロバイダに変更できます。詳しくは<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">こちら</a>をご覧ください。Google経由でOpenIDサービスを設定するには、<a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">こちらの手順</a>に従ってください。",
|
||||
"oauth_description": "OpenIDは、Googleなどの他のサービスのアカウントを使用して web サイトにログインし、本人確認を行うための標準化された方法です。デフォルトの発行者はGoogleですが、他のOpenIDプロバイダに変更できます。詳しくは<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">こちら</a>をご覧ください。Google経由でOpenIDサービスを設定するには、<a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">こちらの手順</a>に従ってください。",
|
||||
"oauth_description_warning": "OAuth/OpenIDを有効にするには、config.iniファイルにOAuth/OpenIDのベースURL、クライアントID、クライアントシークレットを設定し、アプリケーションを再起動する必要があります。環境変数から設定する場合は、TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID and TRILIUM_OAUTH_CLIENT_SECRET を設定してください。",
|
||||
"oauth_missing_vars": "設定がありません: {{-variables}}",
|
||||
"oauth_user_account": "ユーザーアカウント: ",
|
||||
@@ -2116,5 +2126,43 @@
|
||||
"unknown_http_error_title": "サーバーとの通信エラー",
|
||||
"unknown_http_error_content": "ステータスコード: {{statusCode}}\nURL: {{method}} {{url}}\nメッセージ: {{message}}",
|
||||
"traefik_blocks_requests": "Traefik リバース プロキシを使用している場合、サーバーとの通信に影響する重大な変更が導入されました。"
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "前のノートに戻る",
|
||||
"go-forward": "次のノートに進む"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "実験オプション",
|
||||
"disclaimer": "これらのオプションは試験的なもので、動作が不安定になる可能性があります。注意してご使用ください。",
|
||||
"new_layout_name": "新しいレイアウト",
|
||||
"new_layout_description": "よりモダンな外観と使いやすさが向上した新しいレイアウトをお試しください。今後のリリースで大幅な変更が加えられる可能性があります。"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "読み取り専用",
|
||||
"read_only_auto": "自動的に読み取り専用",
|
||||
"shared_publicly": "公開で共有",
|
||||
"shared_locally": "ローカルで共有",
|
||||
"read_only_explicit_description": "このノートは手動で読み取り専用に設定されています。\nクリックすると一時的に編集できます。",
|
||||
"read_only_temporarily_disabled": "一時的に編集可能",
|
||||
"read_only_auto_description": "このノートはパフォーマンス上の理由により、自動的に読み取り専用モードに設定されました。この自動制限は設定から調整できます。\n\n一時的に編集するにはクリックしてください。",
|
||||
"read_only_temporarily_disabled_description": "このノートは現在編集可能ですが、通常は読み取り専用です。別のノートに移動すると読み取り専用に戻ります。\n\nクリックすると読み取り専用モードが再度有効になります。",
|
||||
"shared_publicly_description": "このノートは {{- link}} でオンライン公開されており、誰でも閲覧可能です。\n\n共有ノートに移動するにはクリックするか、右クリックしてその他のオプションを選択してください。",
|
||||
"shared_locally_description": "このノートは、{{- link}} でローカルネットワークのみで共有されています。\n\nクリックして共有ノートに移動するか、右クリックしてその他のオプションを選択してください。",
|
||||
"clipped_note": "Web クリップ",
|
||||
"clipped_note_description": "このノートは {{url}} から取得されました。\n\nクリックすると元の web ページに移動します。",
|
||||
"execute_script": "スクリプトを実行",
|
||||
"execute_script_description": "このノートはスクリプトノートです。クリックするとスクリプトが実行されます。",
|
||||
"execute_sql": "SQL を実行",
|
||||
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。"
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "コンテンツ全体の言語を変更",
|
||||
"note_info_title": "作成日/変更日やノートのサイズなど、このノートに関する情報を表示する。",
|
||||
"backlinks_title_other": "このノートは {{count}} 件の他のノートからリンクされています。\n\nクリックするとバックリンクのリストが表示されます。",
|
||||
"attachments_title_other": "このノートには {{count}} 件の添付ファイルがあります。クリックすると、添付ファイルのリストが新しいタブで開きます。",
|
||||
"attributes_other": "{{count}} 個の属性",
|
||||
"attributes_title": "クリックすると専用のペインが開き、このノートの所有属性を編集したり、継承された属性のリストを表示できます。",
|
||||
"note_paths_title": "クリックすると、このノートがツリー内に配置されているパスが表示されます。",
|
||||
"code_note_switcher": "言語モードを変更"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -493,7 +493,12 @@
|
||||
"editable_text": {
|
||||
"placeholder": "Scrieți conținutul notiței aici...",
|
||||
"auto-detect-language": "Automat",
|
||||
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă."
|
||||
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă.",
|
||||
"editor_crashed_title": "Editorul text a avut o eroare",
|
||||
"editor_crashed_content": "Conținutul a fost recuperat cu succes, dar este posibil ca o parte din cele mai recente modificări ale dvs. să nu se fi salvat.",
|
||||
"editor_crashed_details_button": "Mai multe detalii...",
|
||||
"editor_crashed_details_intro": "Dacă întâmpinați frecvent această eroare, considerați să o raportați pe GitHub copiând informația de mai jos.",
|
||||
"editor_crashed_details_title": "Informații tehnice"
|
||||
},
|
||||
"edited_notes": {
|
||||
"deleted": "(șters)",
|
||||
@@ -785,7 +790,8 @@
|
||||
"info": {
|
||||
"closeButton": "Închide",
|
||||
"modalTitle": "Mesaj informativ",
|
||||
"okButton": "OK"
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "Copiază în clipboard"
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"no_inherited_attributes": "Niciun atribut moștenit.",
|
||||
@@ -867,12 +873,14 @@
|
||||
"print_note": "Imprimare notiță",
|
||||
"re_render_note": "Reinterpretare notiță",
|
||||
"save_revision": "Salvează o nouă revizie",
|
||||
"advanced": "Advansat",
|
||||
"search_in_note": "Caută în notiță",
|
||||
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
|
||||
"convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.",
|
||||
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?",
|
||||
"print_pdf": "Exportare ca PDF...",
|
||||
"open_note_on_server": "Deschide notița pe server"
|
||||
"open_note_on_server": "Deschide notița pe server",
|
||||
"view_revisions": "Revizii ale notițelor..."
|
||||
},
|
||||
"note_erasure_timeout": {
|
||||
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
|
||||
@@ -1407,7 +1415,7 @@
|
||||
"hoist-note": "Focalizează notița",
|
||||
"unhoist-note": "Defocalizează notița",
|
||||
"converted-to-attachments": "{{count}} notițe au fost convertite în atașamente.",
|
||||
"convert-to-attachment-confirm": "Doriți convertirea notițelor selectate în atașamente ale notiței părinte?",
|
||||
"convert-to-attachment-confirm": "Doriți convertirea notițelor selectate în atașamente ale notiței părinte? Această operațiune se aplică doar notițelor de tip imagine, celelalte vor fi ignorate.",
|
||||
"open-in-popup": "Editare rapidă",
|
||||
"archive": "Arhivează",
|
||||
"unarchive": "Dezarhivează"
|
||||
@@ -1526,7 +1534,9 @@
|
||||
"printing_pdf": "Exportare ca PDF în curs..."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "introduceți titlul notiței aici..."
|
||||
"placeholder": "introduceți titlul notiței aici...",
|
||||
"created_on": "Creată la <Value />",
|
||||
"last_modified": "Modificată la <Value />"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"erase_excess_revision_snapshots": "Șterge acum reviziile excesive",
|
||||
@@ -1758,7 +1768,8 @@
|
||||
},
|
||||
"note_language": {
|
||||
"configure-languages": "Configurează limbile...",
|
||||
"not_set": "Nedefinită"
|
||||
"not_set": "Nicio limbă setată",
|
||||
"help-on-languages": "Informații despre limba conținutului..."
|
||||
},
|
||||
"png_export_button": {
|
||||
"button_title": "Exportă diagrama ca PNG"
|
||||
@@ -1954,7 +1965,8 @@
|
||||
"oauth_user_not_logged_in": "Neautentificat!"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "Diagrama nu a putut fi exportată în PNG."
|
||||
"export_to_png": "Diagrama nu a putut fi exportată în PNG.",
|
||||
"export_to_svg": "Diagrama nu a putut fi exportată în SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Afișare",
|
||||
@@ -2106,5 +2118,32 @@
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Comută la editorul principal"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "Opțiuni experimentale",
|
||||
"disclaimer": "Aceste opțiuni sunt experimentale și pot cauza instabilitate. Folosiți cu prudență.",
|
||||
"new_layout_name": "Aspect nou",
|
||||
"new_layout_description": "Încercați noul aspect pentru un design mai modern și mai ușor de utilizat. Poate surveni modificări semnificative în următoarele release-uri."
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "Eroare de comunicare cu server-ul",
|
||||
"unknown_http_error_content": "Cod: {{statusCode}}\nURL: {{method}} {{url}}\nMesaj: {{message}}",
|
||||
"traefik_blocks_requests": "Dacă utilizați reverse proxy-ul Traefik, acesta a introdus o schimbare majoră ce afectează comunicarea cu server-ul."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Înapoi la notița anterioară",
|
||||
"go-forward": "Înainte către notița următoare"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Mod citire",
|
||||
"read_only_explicit_description": "Această notiță a fost setată explicit să fie doar în citire.\nClick pentru a o edita temporar.",
|
||||
"read_only_auto": "Mod citire auto",
|
||||
"read_only_auto_description": "Această notița a fost setată automată să fie în mod doar de citire din motive de performanță. Această limită automată este ajustabilă din setări.\n\nClick pentru a o edita temporar.",
|
||||
"read_only_temporarily_disabled": "Editabilă temporar",
|
||||
"read_only_temporarily_disabled_description": "Această notiță se poate modifica, deși în mod normal ea este doar în citire. Notița va reveni la modul doar în citire imediat ce navigați către altă notiță.\n\nClick pentru a re-activa modul doar în citire.",
|
||||
"shared_publicly": "Partajată public",
|
||||
"shared_publicly_description": "Această notiță este publicată online la {{- link}} și este acesibilă public.\n\nClic pentru a naviga la pagina partajată sau click dreapta pentru mai multe opțiuni.",
|
||||
"shared_locally": "Partajată local",
|
||||
"shared_locally_description": "Această notiță este partajată doar pe rețeaua locală la {{- link}}.\n\nClic pentru a naviga la pagina partajată sau click dreapta pentru mai multe opțiuni."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,8 @@
|
||||
"info": {
|
||||
"modalTitle": "資訊消息",
|
||||
"closeButton": "關閉",
|
||||
"okButton": "確定"
|
||||
"okButton": "確定",
|
||||
"copy_to_clipboard": "複製到剪貼簿"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "全文搜尋",
|
||||
@@ -986,7 +987,12 @@
|
||||
"editable_text": {
|
||||
"placeholder": "在這裡輸入您的筆記內容…",
|
||||
"auto-detect-language": "自動檢測",
|
||||
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在,請考慮提交錯誤報告。"
|
||||
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在,請考慮提交錯誤報告。",
|
||||
"editor_crashed_title": "文字編輯器崩潰",
|
||||
"editor_crashed_content": "您的內容已成功恢復,但最近的幾項變更可能未被儲存。",
|
||||
"editor_crashed_details_button": "檢視更多資訊⋯",
|
||||
"editor_crashed_details_intro": "若您多次遇到此錯誤,請考慮在 GitHub 回報以下資訊。",
|
||||
"editor_crashed_details_title": "技術資訊"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "透過在下面的輸入框中輸入筆記標題或在樹中選擇筆記來打開筆記。",
|
||||
@@ -1531,7 +1537,9 @@
|
||||
"printing_pdf": "正在匯出為 PDF…"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "請輸入筆記標題..."
|
||||
"placeholder": "請輸入筆記標題...",
|
||||
"created_on": "建立於 {{date}}",
|
||||
"last_modified": "最後修改於 {{date}}"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
|
||||
@@ -2106,5 +2114,26 @@
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "切換至完整編輯器"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "實驗性選項",
|
||||
"disclaimer": "這些選項屬實驗性質,可能導致系統不穩定。請謹慎使用。",
|
||||
"new_layout_name": "新版面配置",
|
||||
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。"
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "與伺服器通訊錯誤",
|
||||
"unknown_http_error_content": "狀態碼:{{statusCode}}\n網址:{{method}} {{url}}\n訊息:{{message}}",
|
||||
"traefik_blocks_requests": "若您正在使用 Traefik 反向代理,該代理已引入一項重大變更影響與伺服器的通訊。"
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "返回前一筆記",
|
||||
"go-forward": "前往下一筆記"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "唯讀",
|
||||
"read_only_auto": "自動唯讀",
|
||||
"shared_publicly": "公開分享",
|
||||
"shared_locally": "本地分享"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import NoteContext from "../components/note_context";
|
||||
import FNote from "../entities/fnote";
|
||||
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
|
||||
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
|
||||
import server from "../services/server";
|
||||
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
|
||||
@@ -20,6 +20,7 @@ import RawHtml from "./react/RawHtml";
|
||||
import { ViewTypeOptions } from "./collections/interface";
|
||||
import attributes from "../services/attributes";
|
||||
import LoadResults from "../services/load_results";
|
||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features";
|
||||
|
||||
export interface FloatingButtonContext {
|
||||
parentComponent: Component;
|
||||
@@ -76,6 +77,8 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
ToggleReadOnlyButton
|
||||
];
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
|
||||
return isEnabled && <FloatingButton
|
||||
@@ -297,8 +300,9 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
|
||||
|
||||
function InAppHelpButton({ note }: FloatingButtonContext) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
const isEnabled = !!helpUrl && (!isNewLayout || (note?.type !== "book"));
|
||||
|
||||
return !!helpUrl && (
|
||||
return isEnabled && (
|
||||
<FloatingButton
|
||||
icon="bx bx-help-circle"
|
||||
text={t("help-button.title")}
|
||||
@@ -308,22 +312,9 @@ function InAppHelpButton({ note }: FloatingButtonContext) {
|
||||
}
|
||||
|
||||
function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
let [ backlinkCount, setBacklinkCount ] = useState(0);
|
||||
let [ popupOpen, setPopupOpen ] = useState(false);
|
||||
const [ popupOpen, setPopupOpen ] = useState(false);
|
||||
const backlinksContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function refresh() {
|
||||
if (!isDefaultViewMode) return;
|
||||
|
||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||
setBacklinkCount(resp.count);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => refresh(), [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (needsRefresh(note, loadResults)) refresh();
|
||||
});
|
||||
const backlinkCount = useBacklinkCount(note, isDefaultViewMode);
|
||||
|
||||
// Determine the max height of the container.
|
||||
const { windowHeight } = useWindowSize();
|
||||
@@ -336,7 +327,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
}
|
||||
}, [ popupOpen, windowHeight ]);
|
||||
|
||||
const isEnabled = isDefaultViewMode && backlinkCount > 0;
|
||||
const isEnabled = !isNewLayout && isDefaultViewMode && backlinkCount > 0;
|
||||
return (isEnabled &&
|
||||
<div className="backlinks-widget has-overflow">
|
||||
<div
|
||||
@@ -355,15 +346,34 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksList({ note }: { note: FNote }) {
|
||||
export function useBacklinkCount(note: FNote | null | undefined, isDefaultViewMode: boolean) {
|
||||
const [ backlinkCount, setBacklinkCount ] = useState(0);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (!note || !isDefaultViewMode) return;
|
||||
|
||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||
setBacklinkCount(resp.count);
|
||||
});
|
||||
}, [ isDefaultViewMode, note ]);
|
||||
|
||||
useEffect(() => refresh(), [ refresh ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (note && needsRefresh(note, loadResults)) refresh();
|
||||
});
|
||||
|
||||
return backlinkCount;
|
||||
}
|
||||
|
||||
export function BacklinksList({ note }: { note: FNote }) {
|
||||
const [ backlinks, setBacklinks ] = useState<BacklinksResponse>([]);
|
||||
|
||||
function refresh() {
|
||||
server.get<BacklinksResponse>(`note-map/${note.noteId}/backlinks`).then(async (backlinks) => {
|
||||
// prefetch all
|
||||
const noteIds = backlinks
|
||||
.filter(bl => "noteId" in bl)
|
||||
.map((bl) => bl.noteId);
|
||||
.filter(bl => "noteId" in bl)
|
||||
.map((bl) => bl.noteId);
|
||||
await froca.getNotes(noteIds);
|
||||
setBacklinks(backlinks);
|
||||
});
|
||||
|
||||
@@ -299,8 +299,10 @@ async function getWidgetType(note: FNote | null | undefined, noteContext: NoteCo
|
||||
|
||||
if (noteContext?.viewScope?.viewMode === "source") {
|
||||
resultingType = "readOnlyCode";
|
||||
} else if (noteContext?.viewScope && noteContext.viewScope.viewMode === "attachments") {
|
||||
} else if (noteContext.viewScope?.viewMode === "attachments") {
|
||||
resultingType = noteContext.viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
|
||||
} else if (noteContext.viewScope?.viewMode === "note-map") {
|
||||
resultingType = "noteMap";
|
||||
} else if (type === "text" && (await noteContext?.isReadOnly())) {
|
||||
resultingType = "readOnlyText";
|
||||
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {
|
||||
|
||||
7
apps/client/src/widgets/TabHistoryNavigationButtons.css
Normal file
7
apps/client/src/widgets/TabHistoryNavigationButtons.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.component.tab-history-navigation-buttons {
|
||||
contain: none;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-inline-end: 0.5em;
|
||||
}
|
||||
64
apps/client/src/widgets/TabHistoryNavigationButtons.tsx
Normal file
64
apps/client/src/widgets/TabHistoryNavigationButtons.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import "./TabHistoryNavigationButtons.css";
|
||||
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../services/i18n";
|
||||
import { dynamicRequire, isElectron } from "../services/utils";
|
||||
import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import { useLauncherVisibility } from "./react/hooks";
|
||||
|
||||
export default function TabHistoryNavigationButtons() {
|
||||
const webContents = useMemo(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
|
||||
const onContextMenu = webContents ? handleHistoryContextMenu(webContents) : undefined;
|
||||
const { canGoBack, canGoForward } = useBackForwardState(webContents);
|
||||
const legacyBackVisible = useLauncherVisibility("_lbBackInHistory");
|
||||
const legacyForwardVisible = useLauncherVisibility("_lbForwardInHistory");
|
||||
|
||||
return (isElectron() &&
|
||||
<div className="tab-history-navigation-buttons">
|
||||
{!legacyBackVisible && <ActionButton
|
||||
icon="bx bx-left-arrow-alt"
|
||||
text={t("tab_history_navigation_buttons.go-back")}
|
||||
triggerCommand="backInNoteHistory"
|
||||
onContextMenu={onContextMenu}
|
||||
disabled={!canGoBack}
|
||||
/>}
|
||||
{!legacyForwardVisible && <ActionButton
|
||||
icon="bx bx-right-arrow-alt"
|
||||
text={t("tab_history_navigation_buttons.go-forward")}
|
||||
triggerCommand="forwardInNoteHistory"
|
||||
onContextMenu={onContextMenu}
|
||||
disabled={!canGoForward}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useBackForwardState(webContents: Electron.WebContents | undefined) {
|
||||
const [ canGoBack, setCanGoBack ] = useState(webContents?.navigationHistory.canGoBack());
|
||||
const [ canGoForward, setCanGoForward ] = useState(webContents?.navigationHistory.canGoForward());
|
||||
|
||||
useEffect(() => {
|
||||
if (!webContents) return;
|
||||
|
||||
const updateNavigationState = () => {
|
||||
setCanGoBack(webContents.navigationHistory.canGoBack());
|
||||
setCanGoForward(webContents.navigationHistory.canGoForward());
|
||||
};
|
||||
|
||||
webContents.on("did-navigate", updateNavigationState);
|
||||
webContents.on("did-navigate-in-page", updateNavigationState);
|
||||
|
||||
return () => {
|
||||
webContents.removeListener("did-navigate", updateNavigationState);
|
||||
webContents.removeListener("did-navigate-in-page", updateNavigationState);
|
||||
};
|
||||
}, [ webContents ]);
|
||||
|
||||
if (!webContents) {
|
||||
return { canGoBack: true, canGoForward: true };
|
||||
}
|
||||
|
||||
return { canGoBack, canGoForward };
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import shortcutService from "../../services/shortcuts.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import type { Attribute } from "../../services/attribute_parser.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="attr-detail tn-tool-dialog">
|
||||
@@ -309,6 +310,8 @@ interface SearchRelatedResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
private $title!: JQuery<HTMLElement>;
|
||||
private $inputName!: JQuery<HTMLElement>;
|
||||
@@ -579,6 +582,13 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
|
||||
if (isNewLayout) {
|
||||
this.$widget
|
||||
.css("top", "unset")
|
||||
.css("bottom", 70)
|
||||
.css("max-height", "80vh");
|
||||
}
|
||||
|
||||
if (focus === "name") {
|
||||
this.$inputName.trigger("focus").trigger("select");
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import "./global_menu.css";
|
||||
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool, useTriliumOptionInt } from "../react/hooks";
|
||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem } from "../react/FormList";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import KeyboardShortcut from "../react/KeyboardShortcut";
|
||||
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import Component from "../../components/component";
|
||||
import { ExperimentalFeature, ExperimentalFeatureId, experimentalFeatures, isExperimentalFeatureEnabled, toggleExperimentalFeature } from "../../services/experimental_features";
|
||||
import { t } from "../../services/i18n";
|
||||
import utils, { dynamicRequire, isElectron, isMobile, reloadFrontendApp } from "../../services/utils";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem } from "../react/FormList";
|
||||
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool, useTriliumOptionInt } from "../react/hooks";
|
||||
import KeyboardShortcut from "../react/KeyboardShortcut";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils";
|
||||
|
||||
interface MenuItemProps<T> {
|
||||
icon: string,
|
||||
@@ -70,8 +73,9 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
</>}
|
||||
|
||||
{!isElectron() && <BrowserOnlyOptions />}
|
||||
{glob.isDev && <DevelopmentOptions />}
|
||||
</Dropdown>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedMenu() {
|
||||
@@ -89,7 +93,7 @@ function AdvancedMenu() {
|
||||
{isElectron() && <MenuItem command="openDevTools" icon="bx bx-bug-alt" text={t("global_menu.open_dev_tools")} />}
|
||||
<KeyboardActionMenuItem command="reloadFrontendApp" icon="bx bx-refresh" text={t("global_menu.reload_frontend")} title={t("global_menu.reload_hint")} />
|
||||
</FormDropdownSubmenu>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BrowserOnlyOptions() {
|
||||
@@ -99,6 +103,35 @@ function BrowserOnlyOptions() {
|
||||
</>;
|
||||
}
|
||||
|
||||
function DevelopmentOptions() {
|
||||
const [ layoutOrientation ] = useTriliumOption("layoutOrientation");
|
||||
|
||||
return <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem disabled>Development Options</FormListItem>
|
||||
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={layoutOrientation === "horizontal"}>
|
||||
{experimentalFeatures.map((feature) => (
|
||||
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
|
||||
))}
|
||||
</FormDropdownSubmenu>
|
||||
</>;
|
||||
}
|
||||
|
||||
function ExperimentalFeatureToggle({ experimentalFeature }: { experimentalFeature: ExperimentalFeature }) {
|
||||
const featureEnabled = isExperimentalFeatureEnabled(experimentalFeature.id as ExperimentalFeatureId);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
checked={featureEnabled}
|
||||
title={experimentalFeature.description}
|
||||
onClick={async () => {
|
||||
await toggleExperimentalFeature(experimentalFeature.id as ExperimentalFeatureId, !featureEnabled);
|
||||
reloadFrontendApp();
|
||||
}}
|
||||
>{experimentalFeature.name}</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SwitchToOptions() {
|
||||
if (isElectron()) {
|
||||
return;
|
||||
|
||||
@@ -243,7 +243,7 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
|
||||
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
|
||||
currentValue?: string;
|
||||
placeholder?: string;
|
||||
save: (newValue: string) => void;
|
||||
save: (newValue: string) => void | Promise<void>;
|
||||
dismiss: () => void;
|
||||
isNewItem?: boolean;
|
||||
mode?: "normal" | "multiline" | "relation";
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
|
||||
.floating-buttons-children {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.presentation-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
1
apps/client/src/widgets/containers/content_header.css
Normal file
1
apps/client/src/widgets/containers/content_header.css
Normal file
@@ -0,0 +1 @@
|
||||
/** Intentionally left empty for now **/
|
||||
@@ -5,6 +5,7 @@ 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 { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
|
||||
|
||||
/**
|
||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||
@@ -37,6 +38,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
this.#setBackdropEffects();
|
||||
this.#setThemeCapabilities();
|
||||
this.#setLocaleAndDirection(options.get("locale"));
|
||||
this.#setExperimentalFeatures();
|
||||
|
||||
return super.render();
|
||||
}
|
||||
@@ -56,7 +58,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
|
||||
if (loadResults.isOptionReloaded("maxContentWidth")
|
||||
|| loadResults.isOptionReloaded("centerContent")) {
|
||||
|
||||
|
||||
this.#setMaxContentWidth();
|
||||
}
|
||||
}
|
||||
@@ -99,6 +101,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
|
||||
}
|
||||
|
||||
#setExperimentalFeatures() {
|
||||
for (const featureId of getEnabledExperimentalFeatureIds()) {
|
||||
document.body.classList.add(`experimental-feature-${featureId}`);
|
||||
}
|
||||
}
|
||||
|
||||
#setLocaleAndDirection(locale: string) {
|
||||
const correspondingLocale = LOCALES.find(l => l.id === locale);
|
||||
document.body.lang = locale;
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
|
||||
.note-split.type-code:not(.mime-text-x-sqlite) > .scrolling-container {
|
||||
background-color: var(--code-background-color);
|
||||
}
|
||||
--scrollbar-background-color: var(--main-background-color);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default class ScrollingContainer extends Container<BasicWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerToCommand">) {
|
||||
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerTo">) {
|
||||
this.$widget.scrollTop(position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,12 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-header .note-title-widget {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .modal-body {
|
||||
@@ -42,13 +44,14 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
height: 75vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .title-row,
|
||||
.modal.popup-editor-dialog .modal-title,
|
||||
.modal.popup-editor-dialog .note-icon-widget {
|
||||
height: 32px;
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-icon-widget {
|
||||
@@ -99,7 +102,7 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-code-editor {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
|
||||
& .cm-editor {
|
||||
margin: 0;
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import "./PopupEditor.css";
|
||||
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import { NoteContextContext, ParentComponent } from "../react/react_utils";
|
||||
import NoteDetail from "../NoteDetail";
|
||||
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import tree from "../../services/tree";
|
||||
import utils from "../../services/utils";
|
||||
import NoteList from "../collections/NoteList";
|
||||
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
||||
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||
import PromotedAttributes from "../PromotedAttributes";
|
||||
import FloatingButtons from "../FloatingButtons";
|
||||
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
|
||||
import utils from "../../services/utils";
|
||||
import tree from "../../services/tree";
|
||||
import froca from "../../services/froca";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import NoteDetail from "../NoteDetail";
|
||||
import PromotedAttributes from "../PromotedAttributes";
|
||||
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import { NoteContextContext, ParentComponent } from "../react/react_utils";
|
||||
import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
|
||||
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
||||
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
||||
import { t } from "../../services/i18n";
|
||||
import appContext from "../../components/app_context";
|
||||
import NoteBadges from "../layout/NoteBadges";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function PopupEditor() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
@@ -61,7 +67,10 @@ export default function PopupEditor() {
|
||||
<NoteContextContext.Provider value={noteContext}>
|
||||
<DialogWrapper>
|
||||
<Modal
|
||||
title={<TitleRow />}
|
||||
title={<>
|
||||
<TitleRow />
|
||||
{isNewLayout && <NoteBadges />}
|
||||
</>}
|
||||
customTitleBarButtons={[{
|
||||
iconClassName: "bx-expand-alt",
|
||||
title: t("popup-editor.maximize"),
|
||||
@@ -75,19 +84,17 @@ export default function PopupEditor() {
|
||||
className="popup-editor-dialog"
|
||||
size="lg"
|
||||
show={shown}
|
||||
onShown={() => {
|
||||
parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId });
|
||||
}}
|
||||
onShown={() => parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId })}
|
||||
onHidden={() => setShown(false)}
|
||||
keepInDom // needed for faster loading
|
||||
noFocus // automatic focus breaks block popup
|
||||
>
|
||||
<ReadOnlyNoteInfoBar />
|
||||
{!isNewLayout && <ReadOnlyNoteInfoBar />}
|
||||
<PromotedAttributes />
|
||||
|
||||
{isMobile
|
||||
? <MobileEditorToolbar inPopupEditor />
|
||||
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
|
||||
? <MobileEditorToolbar inPopupEditor />
|
||||
: <StandaloneRibbonAdapter component={FormattingToolbar} />}
|
||||
|
||||
<FloatingButtons items={items} />
|
||||
<NoteDetail />
|
||||
@@ -95,7 +102,7 @@ export default function PopupEditor() {
|
||||
</Modal>
|
||||
</DialogWrapper>
|
||||
</NoteContextContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogWrapper({ children }: { children: ComponentChildren }) {
|
||||
@@ -107,7 +114,7 @@ export function DialogWrapper({ children }: { children: ComponentChildren }) {
|
||||
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function TitleRow() {
|
||||
@@ -116,5 +123,5 @@ export function TitleRow() {
|
||||
<NoteIcon />
|
||||
<NoteTitleWidget />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import note_types from "../../services/note_types";
|
||||
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
||||
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
||||
import { Suggestion } from "../../services/note_autocomplete";
|
||||
import Badge from "../react/Badge";
|
||||
import SimpleBadge from "../react/Badge";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
export interface ChooseNoteTypeResponse {
|
||||
@@ -108,7 +108,7 @@ export default function NoteTypeChooserDialogComponent() {
|
||||
value={[ item.type, item.templateNoteId ].join(",") }
|
||||
icon={item.uiIcon}>
|
||||
{item.title}
|
||||
{item.badges && item.badges.map((badge) => <Badge {...badge} />)}
|
||||
{item.badges && item.badges.map((badge) => <SimpleBadge {...badge} />)}
|
||||
</FormListItem>;
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import type { WebContents } from "electron";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
|
||||
import froca from "../../services/froca";
|
||||
import link from "../../services/link";
|
||||
import tree from "../../services/tree";
|
||||
import { dynamicRequire, isElectron } from "../../services/utils";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import type { WebContents } from "electron";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
|
||||
import tree from "../../services/tree";
|
||||
import link from "../../services/link";
|
||||
|
||||
interface HistoryNavigationProps {
|
||||
launcherNote: FNote;
|
||||
@@ -16,71 +18,64 @@ const HISTORY_LIMIT = 20;
|
||||
|
||||
export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
const webContentsRef = useRef<WebContents>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const webContents = dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
// without this, the history is preserved across frontend reloads
|
||||
webContents?.clearHistory();
|
||||
webContentsRef.current = webContents;
|
||||
}
|
||||
}, []);
|
||||
const webContents = useMemo(() => isElectron() ? dynamicRequire("@electron/remote").getCurrentWebContents() : undefined, []);
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command}
|
||||
onContextMenu={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const webContents = webContentsRef.current;
|
||||
if (!webContents || webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
uiIcon:
|
||||
parseInt(idx) === activeIndex
|
||||
? "bx bx-radio-circle-marked" // compare with type coercion!
|
||||
: parseInt(idx) < activeIndex
|
||||
? "bx bx-left-arrow-alt"
|
||||
: "bx bx-right-arrow-alt"
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > HISTORY_LIMIT) {
|
||||
items = items.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: (item: MenuCommandItem<string>) => {
|
||||
if (item && item.command && webContents) {
|
||||
const idx = parseInt(item.command, 10);
|
||||
webContents.navigationHistory.goToIndex(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
onContextMenu={webContents ? handleHistoryContextMenu(webContents) : undefined}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function handleHistoryContextMenu(webContents: WebContents) {
|
||||
return async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!webContents || webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!noteId || !notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
const index = parseInt(idx, 10);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
checked: index === activeIndex,
|
||||
enabled: index !== activeIndex,
|
||||
uiIcon: note?.getIcon()
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > HISTORY_LIMIT) {
|
||||
items = items.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: (item: MenuCommandItem<string>) => {
|
||||
if (item && item.command && webContents) {
|
||||
const idx = parseInt(item.command, 10);
|
||||
webContents.navigationHistory.goToIndex(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
62
apps/client/src/widgets/layout/Breadcrumb.css
Normal file
62
apps/client/src/widgets/layout/Breadcrumb.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.breadcrumb {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
gap: 0.25em;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
> span,
|
||||
> span > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
min-width: 0;
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
flex-shrink: 2;
|
||||
}
|
||||
}
|
||||
|
||||
> span:last-of-type a {
|
||||
max-width: 300px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
ul {
|
||||
flex-direction: column;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropdown-item span,
|
||||
.dropdown-item strong,
|
||||
.breadcrumb-last-item {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.breadcrumb-last-item {
|
||||
text-decoration: none;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0 10px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
198
apps/client/src/widgets/layout/Breadcrumb.tsx
Normal file
198
apps/client/src/widgets/layout/Breadcrumb.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import "./Breadcrumb.css";
|
||||
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import link_context_menu from "../../menus/link_context_menu";
|
||||
import froca from "../../services/froca";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormListItem } from "../react/FormList";
|
||||
import { useChildNotes, useNoteLabel, useNoteProperty } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
|
||||
const COLLAPSE_THRESHOLD = 5;
|
||||
const INITIAL_ITEMS = 2;
|
||||
const FINAL_ITEMS = 2;
|
||||
|
||||
export default function Breadcrumb({ note, noteContext }: { note: FNote, noteContext: NoteContext }) {
|
||||
const notePath = buildNotePaths(noteContext?.notePathArray);
|
||||
|
||||
return (
|
||||
<div className="breadcrumb">
|
||||
{notePath.length > COLLAPSE_THRESHOLD ? (
|
||||
<>
|
||||
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
|
||||
<Fragment key={item}>
|
||||
<BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
|
||||
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />
|
||||
</Fragment>
|
||||
))}
|
||||
<BreadcrumbCollapsed items={notePath.slice(INITIAL_ITEMS, -FINAL_ITEMS)} noteContext={noteContext} />
|
||||
{notePath.slice(-FINAL_ITEMS).map((item, index) => (
|
||||
<Fragment key={item}>
|
||||
<BreadcrumbSeparator notePath={notePath[notePath.length - FINAL_ITEMS - (1 - index)]} activeNotePath={item} noteContext={noteContext} />
|
||||
<BreadcrumbItem index={notePath.length - FINAL_ITEMS + index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
notePath.map((item, index) => (
|
||||
<Fragment key={item}>
|
||||
{index === 0
|
||||
? <BreadcrumbRoot noteContext={noteContext} />
|
||||
: <BreadcrumbItem index={index} notePath={item} notePathLength={notePath.length} noteContext={noteContext} />
|
||||
}
|
||||
{(index < notePath.length - 1 || note?.hasChildren()) &&
|
||||
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />}
|
||||
</Fragment>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined }) {
|
||||
const note = useMemo(() => froca.getNoteFromCache("root"), []);
|
||||
useNoteLabel(note, "iconClass");
|
||||
const title = useNoteProperty(note, "title");
|
||||
|
||||
return (note &&
|
||||
<ActionButton
|
||||
className="root-note"
|
||||
icon={note.getIcon()}
|
||||
text={title ?? ""}
|
||||
onClick={() => noteContext?.setNote("root")}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
link_context_menu.openContextMenu(note.noteId, e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({ notePath }: { notePath: string }) {
|
||||
return (
|
||||
<NoteLink
|
||||
notePath={notePath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLastItem({ notePath }: { notePath: string }) {
|
||||
const noteId = notePath.split("/").at(-1);
|
||||
const [ note ] = useState(() => froca.getNoteFromCache(noteId!));
|
||||
const title = useNoteProperty(note, "title");
|
||||
|
||||
if (!note) return null;
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="breadcrumb-last-item tn-link"
|
||||
onClick={() => {
|
||||
const activeNtxId = appContext.tabManager.activeNtxId;
|
||||
const scrollingContainer = document.querySelector(`[data-ntx-id="${activeNtxId}"] .scrolling-container`);
|
||||
scrollingContainer?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}}
|
||||
>{title}</a>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { index: number, notePathLength: number, notePath: string, noteContext: NoteContext | undefined }) {
|
||||
if (index === 0) {
|
||||
return <BreadcrumbRoot noteContext={noteContext} />;
|
||||
}
|
||||
|
||||
if (index === notePathLength - 1) {
|
||||
return <>
|
||||
<BreadcrumbLastItem notePath={notePath} />
|
||||
</>;
|
||||
}
|
||||
|
||||
return <BreadcrumbLink notePath={notePath} />;
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
|
||||
return (
|
||||
<Dropdown
|
||||
text={<Icon icon="bx bx-chevron-right" />}
|
||||
noSelectButtonStyle
|
||||
buttonClassName="icon-action"
|
||||
hideToggleArrow
|
||||
dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }}
|
||||
>
|
||||
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
|
||||
const notePathComponents = notePath.split("/");
|
||||
const parentNoteId = notePathComponents.at(-1);
|
||||
const childNotes = useChildNotes(parentNoteId);
|
||||
|
||||
return (
|
||||
<ul className="breadcrumb-child-list">
|
||||
{childNotes.map((note) => {
|
||||
const childNotePath = `${notePath}/${note.noteId}`;
|
||||
return <li key={note.noteId}>
|
||||
<FormListItem
|
||||
icon={note.getIcon()}
|
||||
onClick={() => noteContext?.setNote(childNotePath)}
|
||||
>
|
||||
{childNotePath !== activeNotePath
|
||||
? <span>{note.title}</span>
|
||||
: <strong>{note.title}</strong>}
|
||||
</FormListItem>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbCollapsed({ items, noteContext }: { items: string[], noteContext: NoteContext | undefined }) {
|
||||
return (
|
||||
<Dropdown
|
||||
text={<Icon icon="bx bx-dots-horizontal-rounded" />}
|
||||
noSelectButtonStyle
|
||||
buttonClassName="icon-action"
|
||||
hideToggleArrow
|
||||
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
|
||||
>
|
||||
<ul className="breadcrumb-child-list">
|
||||
{items.map((notePath) => {
|
||||
const notePathComponents = notePath.split("/");
|
||||
const noteId = notePathComponents[notePathComponents.length - 1];
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
if (!note) return null;
|
||||
|
||||
return <li key={note.noteId}>
|
||||
<FormListItem
|
||||
icon={note.getIcon()}
|
||||
onClick={() => noteContext?.setNote(notePath)}
|
||||
>
|
||||
<span>{note.title}</span>
|
||||
</FormListItem>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function buildNotePaths(notePathArray: string[] | undefined) {
|
||||
if (!notePathArray) return [];
|
||||
|
||||
let prefix = "";
|
||||
const output: string[] = [];
|
||||
for (const notePath of notePathArray) {
|
||||
output.push(`${prefix}${notePath}`);
|
||||
prefix += `${notePath}/`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
91
apps/client/src/widgets/layout/InlineTitle.css
Normal file
91
apps/client/src/widgets/layout/InlineTitle.css
Normal file
@@ -0,0 +1,91 @@
|
||||
:root {
|
||||
--title-transition: opacity 200ms ease-in;
|
||||
}
|
||||
|
||||
.component.inline-title {
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.inline-title {
|
||||
max-width: var(--max-content-width);
|
||||
padding-inline-start: 24px;
|
||||
|
||||
& > .inline-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--title-transition);
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-icon-widget {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inline-title-row {
|
||||
border-bottom: 2px solid gray;
|
||||
}
|
||||
}
|
||||
|
||||
.title-row {
|
||||
&.note-icon-widget,
|
||||
&.note-title-widget {
|
||||
transition: var(--title-transition);
|
||||
}
|
||||
|
||||
&.hide-title .note-icon-widget,
|
||||
&.hide-title .note-title-widget {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-split.type-code:not(.mime-text-x-sqlite) .inline-title {
|
||||
background-color: var(--main-background-color);
|
||||
}
|
||||
|
||||
body.prefers-centered-content .inline-title {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.title-details {
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
list-style-type: none;
|
||||
opacity: .5;
|
||||
|
||||
span.value {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.note-type-switcher {
|
||||
padding: .25em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
min-height: 40px;
|
||||
--badge-radius: 12px;
|
||||
|
||||
>* {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ext-badge {
|
||||
--color: var(--input-background-color);
|
||||
color: var(--main-text-color);
|
||||
font-size: 0.9rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
303
apps/client/src/widgets/layout/InlineTitle.tsx
Normal file
303
apps/client/src/widgets/layout/InlineTitle.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import "./InlineTitle.css";
|
||||
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChild } from "preact";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ViewScope } from "../../services/link";
|
||||
import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types";
|
||||
import server from "../../services/server";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import { Badge, BadgeWithDropdown } from "../react/Badge";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import { useNoteMetadata } from "../ribbon/NoteInfoTab";
|
||||
import { onWheelHorizontalScroll } from "../widget_utils";
|
||||
|
||||
const supportedNoteTypes = new Set<NoteType>([
|
||||
"text", "code"
|
||||
]);
|
||||
|
||||
export default function InlineTitle() {
|
||||
const { note, parentComponent, viewScope } = useNoteContext();
|
||||
const type = useNoteProperty(note, "type");
|
||||
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ titleHidden, setTitleHidden ] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setShown(shouldShow(note?.noteId, type, viewScope));
|
||||
}, [ note, type, viewScope ]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!shown) return;
|
||||
|
||||
const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector(":scope > .title-row");
|
||||
if (!titleRow) return;
|
||||
|
||||
titleRow.classList.toggle("hide-title", true);
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
titleRow.classList.toggle("hide-title", entries[0].isIntersecting);
|
||||
setTitleHidden(!entries[0].isIntersecting);
|
||||
}, {
|
||||
threshold: 0.85
|
||||
});
|
||||
if (containerRef.current) {
|
||||
observer.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
titleRow.classList.remove("hide-title");
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [ shown, parentComponent ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx("inline-title", !shown && "hidden")}
|
||||
>
|
||||
<div class={clsx("inline-title-row", titleHidden && "hidden")}>
|
||||
<NoteIcon />
|
||||
<NoteTitleWidget />
|
||||
</div>
|
||||
|
||||
<NoteTitleDetails />
|
||||
<NoteTypeSwitcher />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||
if (viewScope?.viewMode !== "default") return false;
|
||||
if (noteId?.startsWith("_options")) return true;
|
||||
return type && supportedNoteTypes.has(type);
|
||||
}
|
||||
|
||||
//#region Title details
|
||||
export function NoteTitleDetails() {
|
||||
const { note } = useNoteContext();
|
||||
const { metadata } = useNoteMetadata(note);
|
||||
const isHiddenNote = note?.noteId.startsWith("_");
|
||||
|
||||
const items: ComponentChild[] = [
|
||||
(!isHiddenNote && metadata?.dateCreated &&
|
||||
<TextWithValue
|
||||
i18nKey="note_title.created_on"
|
||||
value={formatDateTime(metadata.dateCreated, "medium", "none")}
|
||||
valueTooltip={formatDateTime(metadata.dateCreated, "full", "long")}
|
||||
/>),
|
||||
(!isHiddenNote && metadata?.dateModified &&
|
||||
<TextWithValue
|
||||
i18nKey="note_title.last_modified"
|
||||
value={formatDateTime(metadata.dateModified, "medium", "none")}
|
||||
valueTooltip={formatDateTime(metadata.dateModified, "full", "long")}
|
||||
/>)
|
||||
].filter(item => !!item);
|
||||
|
||||
return items.length > 0 && (
|
||||
<div className="title-details">
|
||||
{joinElements(items, " • ")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextWithValue({ i18nKey, value, valueTooltip }: {
|
||||
i18nKey: string;
|
||||
value: string;
|
||||
valueTooltip: string;
|
||||
}) {
|
||||
const listItemRef = useRef<HTMLLIElement>(null);
|
||||
useStaticTooltip(listItemRef, {
|
||||
selector: "span.value",
|
||||
title: valueTooltip,
|
||||
popperConfig: { placement: "bottom" }
|
||||
});
|
||||
|
||||
return (
|
||||
<li ref={listItemRef}>
|
||||
<Trans
|
||||
i18nKey={i18nKey}
|
||||
components={{
|
||||
Value: <span className="value">{value}</span> as React.ReactElement
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Note type switcher
|
||||
const SWITCHER_PINNED_NOTE_TYPES = new Set<NoteType>([ "text", "code", "book", "canvas" ]);
|
||||
|
||||
function NoteTypeSwitcher() {
|
||||
const { note } = useNoteContext();
|
||||
const blob = useNoteBlob(note);
|
||||
const currentNoteType = useNoteProperty(note, "type");
|
||||
const { pinnedNoteTypes, restNoteTypes } = useMemo(() => {
|
||||
const pinnedNoteTypes: NoteTypeMapping[] = [];
|
||||
const restNoteTypes: NoteTypeMapping[] = [];
|
||||
for (const noteType of NOTE_TYPES) {
|
||||
if (noteType.reserved || noteType.static || noteType.type === "book") continue;
|
||||
if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) {
|
||||
pinnedNoteTypes.push(noteType);
|
||||
} else {
|
||||
restNoteTypes.push(noteType);
|
||||
}
|
||||
}
|
||||
return { pinnedNoteTypes, restNoteTypes };
|
||||
}, []);
|
||||
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
||||
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
|
||||
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
|
||||
<div
|
||||
className="note-type-switcher"
|
||||
onWheel={onWheelHorizontalScroll}
|
||||
>
|
||||
{note && blob?.contentLength === 0 && (
|
||||
<>
|
||||
<div className="intro">{t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}</div>
|
||||
{pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && (
|
||||
<Badge
|
||||
key={noteType.type}
|
||||
text={noteType.title}
|
||||
icon={`bx ${noteType.icon}`}
|
||||
onClick={() => switchNoteType(note.noteId, noteType)}
|
||||
/>
|
||||
))}
|
||||
{collectionTemplates.length > 0 && <CollectionNoteTypes noteId={note.noteId} collectionTemplates={collectionTemplates} />}
|
||||
{builtinTemplates.length > 0 && <TemplateNoteTypes noteId={note.noteId} builtinTemplates={builtinTemplates} />}
|
||||
{restNoteTypes.length > 0 && <MoreNoteTypes noteId={note.noteId} restNoteTypes={restNoteTypes} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) {
|
||||
return (
|
||||
<BadgeWithDropdown
|
||||
text={t("note_title.note_type_switcher_others")}
|
||||
icon="bx bx-dots-vertical-rounded"
|
||||
>
|
||||
{restNoteTypes.map(noteType => (
|
||||
<FormListItem
|
||||
key={noteType.type}
|
||||
icon={`bx ${noteType.icon}`}
|
||||
onClick={() => switchNoteType(noteId, noteType)}
|
||||
>{noteType.title}</FormListItem>
|
||||
))}
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) {
|
||||
return (
|
||||
<BadgeWithDropdown
|
||||
text={t("note_title.note_type_switcher_collection")}
|
||||
icon="bx bx-book"
|
||||
>
|
||||
{collectionTemplates.map(collectionTemplate => (
|
||||
<FormListItem
|
||||
key={collectionTemplate.noteId}
|
||||
icon={collectionTemplate.getIcon()}
|
||||
onClick={() => setTemplate(noteId, collectionTemplate.noteId)}
|
||||
>{collectionTemplate.title}</FormListItem>
|
||||
))}
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) {
|
||||
const [ userTemplates, setUserTemplates ] = useState<FNote[]>([]);
|
||||
|
||||
async function refreshTemplates() {
|
||||
const templateNoteIds = await server.get<string[]>("search-templates");
|
||||
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||
setUserTemplates(templateNotes);
|
||||
}
|
||||
|
||||
// First load.
|
||||
useEffect(() => {
|
||||
refreshTemplates();
|
||||
}, []);
|
||||
|
||||
// React to external changes.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) {
|
||||
refreshTemplates();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BadgeWithDropdown
|
||||
text={t("note_title.note_type_switcher_templates")}
|
||||
icon="bx bx-copy-alt"
|
||||
>
|
||||
{userTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
|
||||
{userTemplates.length > 0 && <FormDropdownDivider />}
|
||||
{builtinTemplates.map(template => <TemplateItem key={template.noteId} noteId={noteId} template={template} />)}
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) {
|
||||
return (
|
||||
<FormListItem
|
||||
icon={template.getIcon()}
|
||||
onClick={() => setTemplate(noteId, template.noteId)}
|
||||
>{template.title}</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) {
|
||||
return server.put(`notes/${noteId}/type`, { type, mime });
|
||||
}
|
||||
|
||||
function setTemplate(noteId: string, templateId: string) {
|
||||
return attributes.setRelation(noteId, "template", templateId);
|
||||
}
|
||||
|
||||
function useBuiltinTemplates() {
|
||||
const [ templates, setTemplates ] = useState<{
|
||||
builtinTemplates: FNote[];
|
||||
collectionTemplates: FNote[];
|
||||
}>({
|
||||
builtinTemplates: [],
|
||||
collectionTemplates: []
|
||||
});
|
||||
|
||||
async function loadBuiltinTemplates() {
|
||||
const templatesRoot = await froca.getNote("_templates");
|
||||
if (!templatesRoot) return;
|
||||
const childNotes = await templatesRoot.getChildNotes();
|
||||
const builtinTemplates: FNote[] = [];
|
||||
const collectionTemplates: FNote[] = [];
|
||||
for (const childNote of childNotes) {
|
||||
if (!childNote.hasLabel("template")) continue;
|
||||
if (childNote.hasLabel("collection")) {
|
||||
collectionTemplates.push(childNote);
|
||||
} else {
|
||||
builtinTemplates.push(childNote);
|
||||
}
|
||||
}
|
||||
setTemplates({ builtinTemplates, collectionTemplates });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadBuiltinTemplates();
|
||||
}, []);
|
||||
|
||||
return templates;
|
||||
}
|
||||
//#endregion
|
||||
27
apps/client/src/widgets/layout/NoteBadges.css
Normal file
27
apps/client/src/widgets/layout/NoteBadges.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.component.note-badges {
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.note-badges {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
--badge-radius: 12px;
|
||||
|
||||
.ext-badge {
|
||||
&.temporarily-editable-badge { --color: #4fa52b; }
|
||||
&.read-only-badge { --color: #e33f3b; }
|
||||
&.share-badge { --color: #3b82f6; }
|
||||
&.clipped-note-badge { --color: #57a2a5; }
|
||||
&.execute-badge { --color: #f59e0b; }
|
||||
}
|
||||
|
||||
.dropdown-badge {
|
||||
&.dropdown-backlinks-badge .dropdown-menu {
|
||||
min-width: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
apps/client/src/widgets/layout/NoteBadges.tsx
Normal file
100
apps/client/src/widgets/layout/NoteBadges.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import "./NoteBadges.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren, MouseEventHandler } from "preact";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { useShareInfo } from "../shared_info";
|
||||
import { Badge } from "../react/Badge";
|
||||
|
||||
export default function NoteBadges() {
|
||||
return (
|
||||
<div className="note-badges">
|
||||
<ReadOnlyBadge />
|
||||
<ShareBadge />
|
||||
<ClippedNoteBadge />
|
||||
<ExecuteBadge />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyBadge() {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
||||
const isTemporarilyEditable = noteContext?.ntxId !== "_popup-editor" && noteContext?.viewScope?.readOnlyTemporarilyDisabled;
|
||||
|
||||
if (isTemporarilyEditable) {
|
||||
return <Badge
|
||||
icon="bx bx-lock-open-alt"
|
||||
text={t("breadcrumb_badges.read_only_temporarily_disabled")}
|
||||
tooltip={t("breadcrumb_badges.read_only_temporarily_disabled_description")}
|
||||
className="temporarily-editable-badge"
|
||||
onClick={() => enableEditing(false)}
|
||||
/>;
|
||||
} else if (isReadOnly) {
|
||||
return <Badge
|
||||
icon="bx bx-lock-alt"
|
||||
text={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")}
|
||||
tooltip={isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit_description") : t("breadcrumb_badges.read_only_auto_description")}
|
||||
className="read-only-badge"
|
||||
onClick={() => enableEditing()}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
function ShareBadge() {
|
||||
const { note } = useNoteContext();
|
||||
const { isSharedExternally, link, linkHref } = useShareInfo(note);
|
||||
|
||||
return (link &&
|
||||
<Badge
|
||||
icon={isSharedExternally ? "bx bx-world" : "bx bx-share-alt"}
|
||||
text={isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")}
|
||||
tooltip={isSharedExternally ?
|
||||
t("breadcrumb_badges.shared_publicly_description", { link }) :
|
||||
t("breadcrumb_badges.shared_locally_description", { link })
|
||||
}
|
||||
className="share-badge"
|
||||
href={linkHref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ClippedNoteBadge() {
|
||||
const { note } = useNoteContext();
|
||||
const [ pageUrl ] = useNoteLabel(note, "pageUrl");
|
||||
|
||||
return (pageUrl &&
|
||||
<Badge
|
||||
className="clipped-note-badge"
|
||||
icon="bx bx-globe"
|
||||
text={t("breadcrumb_badges.clipped_note")}
|
||||
tooltip={t("breadcrumb_badges.clipped_note_description", { url: pageUrl })}
|
||||
href={pageUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ExecuteBadge() {
|
||||
const { note, parentComponent } = useNoteContext();
|
||||
const isScript = note?.isTriliumScript();
|
||||
const isSql = note?.isTriliumSqlite();
|
||||
const isExecutable = isScript || isSql;
|
||||
const [ executeDescription ] = useNoteLabel(note, "executeDescription");
|
||||
const [ executeButton ] = useNoteLabelBoolean(note, "executeButton");
|
||||
|
||||
return (note && isExecutable && (executeDescription || executeButton) &&
|
||||
<Badge
|
||||
className="execute-badge"
|
||||
icon="bx bx-play"
|
||||
text={isScript ? t("breadcrumb_badges.execute_script") : t("breadcrumb_badges.execute_sql")}
|
||||
tooltip={executeDescription || (isScript ? t("breadcrumb_badges.execute_script_description") : t("breadcrumb_badges.execute_sql_description"))}
|
||||
onClick={() => parentComponent.triggerCommand("runActiveNote")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
28
apps/client/src/widgets/layout/NoteTitleActions.css
Normal file
28
apps/client/src/widgets/layout/NoteTitleActions.css
Normal file
@@ -0,0 +1,28 @@
|
||||
body.experimental-feature-new-layout {
|
||||
.component.title-actions {
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.title-actions {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
padding-inline-start: 15px;
|
||||
padding-bottom: 0.2em;
|
||||
font-size: 0.8em;
|
||||
|
||||
.dropdown-menu {
|
||||
input.form-control {
|
||||
padding: 2px 8px;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
apps/client/src/widgets/layout/NoteTitleActions.tsx
Normal file
15
apps/client/src/widgets/layout/NoteTitleActions.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import CollectionProperties from "../note_bars/CollectionProperties";
|
||||
import { useNoteContext, useNoteProperty } from "../react/hooks";
|
||||
import "./NoteTitleActions.css";
|
||||
|
||||
export default function NoteTitleActions() {
|
||||
const { note } = useNoteContext();
|
||||
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
|
||||
return (
|
||||
<div className="title-actions">
|
||||
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
apps/client/src/widgets/layout/StatusBar.css
Normal file
114
apps/client/src/widgets/layout/StatusBar.css
Normal file
@@ -0,0 +1,114 @@
|
||||
.component.status-bar {
|
||||
contain: none;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
background-color: var(--left-pane-background-color);
|
||||
|
||||
> .status-bar-main-row {
|
||||
min-height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 0.25em;
|
||||
font-size: 0.85em;
|
||||
|
||||
> .breadcrumb {
|
||||
flex-grow: 1;
|
||||
--icon-button-size: 23px;
|
||||
}
|
||||
|
||||
> .actions-row {
|
||||
padding: 0.1em;
|
||||
display: flex;
|
||||
gap: 0.1em;
|
||||
|
||||
.btn {
|
||||
padding: 0 0.5em !important;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
|
||||
span:first-of-type {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&.dropdown-toggle.show,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: var(--input-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar-dropdown-button {
|
||||
&:after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.dropdown-toggle {
|
||||
padding: 0.1em 0.25em;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-note-info {
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
display: table;
|
||||
|
||||
li {
|
||||
display: table-row;
|
||||
|
||||
> strong {
|
||||
display: table-cell;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
user-select: text;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-note-paths {
|
||||
.note-paths-widget {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.note-path-list {
|
||||
margin: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-code-note-switcher {
|
||||
max-height: 90vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
> .attribute-list {
|
||||
font-size: 0.9em;
|
||||
padding: 0.5em 0.75em;
|
||||
|
||||
.inherited-attributes-widget > div {
|
||||
padding: 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.attribute-list-editor {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
381
apps/client/src/widgets/layout/StatusBar.tsx
Normal file
381
apps/client/src/widgets/layout/StatusBar.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
import "./StatusBar.css";
|
||||
|
||||
import { Locale } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { type ComponentChildren } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ViewScope } from "../../services/link";
|
||||
import server from "../../services/server";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
||||
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useActiveNoteContext, useLegacyImperativeHandlers, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
|
||||
import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor";
|
||||
import InheritedAttributesTab from "../ribbon/InheritedAttributesTab";
|
||||
import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab";
|
||||
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
|
||||
import { useAttachments } from "../type_widgets/Attachment";
|
||||
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
||||
import Breadcrumb from "./Breadcrumb";
|
||||
|
||||
interface StatusBarContext {
|
||||
note: FNote;
|
||||
notePath: string | null | undefined;
|
||||
noteContext: NoteContext;
|
||||
viewScope?: ViewScope;
|
||||
hoistedNoteId?: string;
|
||||
}
|
||||
|
||||
export default function StatusBar() {
|
||||
const { note, notePath, noteContext, viewScope, hoistedNoteId } = useActiveNoteContext();
|
||||
const [ attributesShown, setAttributesShown ] = useState(false);
|
||||
const context: StatusBarContext | undefined | null = note && noteContext && { note, notePath, noteContext, viewScope, hoistedNoteId };
|
||||
const attributesContext: AttributesProps | undefined | null = context && { ...context, attributesShown, setAttributesShown };
|
||||
const isHiddenNote = note?.isInHiddenSubtree();
|
||||
|
||||
return (
|
||||
<div className="status-bar">
|
||||
{attributesContext && <AttributesPane {...attributesContext} />}
|
||||
|
||||
<div className="status-bar-main-row">
|
||||
{context && attributesContext && <>
|
||||
<Breadcrumb {...context} />
|
||||
|
||||
<div className="actions-row">
|
||||
<CodeNoteSwitcher {...context} />
|
||||
<LanguageSwitcher {...context} />
|
||||
{!isHiddenNote && <NotePaths {...context} />}
|
||||
<AttributesButton {...attributesContext} />
|
||||
<AttachmentCount {...context} />
|
||||
<BacklinksBadge {...context} />
|
||||
<NoteInfoBadge {...context} />
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions, dropdownOptions, ...dropdownProps }: Omit<DropdownProps, "hideToggleArrow" | "title" | "titlePosition"> & {
|
||||
title: string;
|
||||
icon?: string;
|
||||
}) {
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName={clsx("status-bar-dropdown-button", buttonClassName)}
|
||||
titlePosition="top"
|
||||
titleOptions={{
|
||||
popperConfig: {
|
||||
...titleOptions?.popperConfig,
|
||||
strategy: "fixed"
|
||||
},
|
||||
...titleOptions
|
||||
}}
|
||||
dropdownOptions={{
|
||||
popperConfig: {
|
||||
strategy: "fixed",
|
||||
placement: "top"
|
||||
},
|
||||
...dropdownOptions
|
||||
}}
|
||||
text={<>
|
||||
{icon && (<><Icon icon={icon} /> </>)}
|
||||
{text}
|
||||
</>}
|
||||
{...dropdownProps}
|
||||
>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
interface StatusBarButtonBaseProps {
|
||||
className?: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
text?: string | number;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
type StatusBarButtonWithCommand = StatusBarButtonBaseProps & { triggerCommand: CommandNames; };
|
||||
type StatusBarButtonWithClick = StatusBarButtonBaseProps & { onClick: () => void; };
|
||||
|
||||
function StatusBarButton({ className, icon, text, title, active, ...restProps }: StatusBarButtonWithCommand | StatusBarButtonWithClick) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useStaticTooltip(buttonRef, {
|
||||
placement: "top",
|
||||
fallbackPlacements: [ "top" ],
|
||||
popperConfig: { strategy: "fixed" },
|
||||
title
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={clsx("btn select-button focus-outline", className, active && "active")}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if ("triggerCommand" in restProps) {
|
||||
parentComponent?.triggerCommand(restProps.triggerCommand);
|
||||
} else {
|
||||
restProps.onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} /> {text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
//#region Language Switcher
|
||||
function LanguageSwitcher({ note }: StatusBarContext) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
||||
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
|
||||
|
||||
return (
|
||||
<>
|
||||
{note.type === "text" && <StatusBarDropdown
|
||||
icon="bx bx-globe"
|
||||
title={t("status_bar.language_title")}
|
||||
text={<span dir={activeLocale?.rtl ? "rtl" : "ltr"}>{getLocaleName(activeLocale)}</span>}
|
||||
>
|
||||
{processedLocales.map((locale, index) =>
|
||||
(typeof locale === "object") ? (
|
||||
<FormListItem
|
||||
key={locale.id}
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentNoteLanguage}
|
||||
onClick={() => setCurrentNoteLanguage(locale.id)}
|
||||
>{locale.name}</FormListItem>
|
||||
) : (
|
||||
<FormDropdownDivider key={`divider-${index}`} />
|
||||
)
|
||||
)}
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
onClick={() => openInAppHelpFromUrl("veGu4faJErEM")}
|
||||
icon="bx bx-help-circle"
|
||||
>{t("note_language.help-on-languages")}</FormListItem>
|
||||
<FormListItem
|
||||
onClick={() => setModalShown(true)}
|
||||
icon="bx bx-cog"
|
||||
>{t("note_language.configure-languages")}</FormListItem>
|
||||
</StatusBarDropdown>}
|
||||
{createPortal(
|
||||
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocaleName(locale: Locale | null | undefined) {
|
||||
if (!locale) return "";
|
||||
if (!locale.id) return "-";
|
||||
if (locale.name.length <= 4 || locale.rtl) return locale.name; // Some locales like Japanese and Chinese look better than their ID.
|
||||
return locale.id
|
||||
.replace("_", "-")
|
||||
.toLocaleUpperCase();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Note info
|
||||
export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
|
||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||
|
||||
return (note &&
|
||||
<StatusBarDropdown
|
||||
icon="bx bx-info-circle"
|
||||
title={t("status_bar.note_info_title")}
|
||||
dropdownContainerClassName="dropdown-note-info"
|
||||
dropdownOptions={{ autoClose: "outside" }}
|
||||
>
|
||||
<ul>
|
||||
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
||||
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
|
||||
<NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} />
|
||||
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
|
||||
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
|
||||
</ul>
|
||||
</StatusBarDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) {
|
||||
return (
|
||||
<li>
|
||||
<strong title={title}>{text}{": "}</strong>
|
||||
<span>{value}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Backlinks
|
||||
function BacklinksBadge({ note, viewScope }: StatusBarContext) {
|
||||
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
return (note && count > 0 &&
|
||||
<StatusBarDropdown
|
||||
className="backlinks-badge backlinks-widget"
|
||||
icon="bx bx-link"
|
||||
text={count}
|
||||
title={t("status_bar.backlinks_title", { count })}
|
||||
dropdownContainerClassName="backlinks-items"
|
||||
>
|
||||
<BacklinksList note={note} />
|
||||
</StatusBarDropdown>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Attachment count
|
||||
function AttachmentCount({ note }: StatusBarContext) {
|
||||
const attachments = useAttachments(note);
|
||||
const count = attachments.length;
|
||||
|
||||
return (note && count > 0 &&
|
||||
<StatusBarButton
|
||||
className="attachment-count-button"
|
||||
icon="bx bx-paperclip"
|
||||
text={count}
|
||||
title={t("status_bar.attachments_title", { count })}
|
||||
triggerCommand="showAttachments"
|
||||
/>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Attributes
|
||||
interface AttributesProps extends StatusBarContext {
|
||||
attributesShown: boolean;
|
||||
setAttributesShown: (shown: boolean) => void;
|
||||
}
|
||||
|
||||
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const [ count, setCount ] = useState(note.attributes.length);
|
||||
|
||||
// React to note changes.
|
||||
useEffect(() => {
|
||||
setCount(note.attributes.length);
|
||||
}, [ note ]);
|
||||
|
||||
// React to changes in count.
|
||||
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
setCount(note.attributes.length);
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<StatusBarButton
|
||||
className="attributes-button"
|
||||
icon="bx bx-list-check"
|
||||
title={t("status_bar.attributes_title")}
|
||||
text={t("status_bar.attributes", { count })}
|
||||
active={attributesShown}
|
||||
onClick={() => setAttributesShown(!attributesShown)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttributesPane({ note, noteContext, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const api = useRef<AttributeEditorImperativeHandlers>(null);
|
||||
|
||||
const context = parentComponent && {
|
||||
componentId: parentComponent.componentId,
|
||||
note,
|
||||
hidden: !note
|
||||
};
|
||||
|
||||
// Show on keyboard shortcuts.
|
||||
useTriliumEvents([ "addNewLabel", "addNewRelation" ], () => setAttributesShown(true));
|
||||
|
||||
// Interaction with the attribute editor.
|
||||
useLegacyImperativeHandlers(useMemo(() => ({
|
||||
saveAttributesCommand: () => api.current?.save(),
|
||||
reloadAttributesCommand: () => api.current?.refresh(),
|
||||
updateAttributeListCommand: ({ attributes }) => api.current?.renderOwnedAttributes(attributes)
|
||||
}), [ api ]));
|
||||
|
||||
return (context &&
|
||||
<div className={clsx("attribute-list", !attributesShown && "hidden-ext")}>
|
||||
<InheritedAttributesTab {...context} />
|
||||
|
||||
<AttributeEditor
|
||||
{...context}
|
||||
api={api}
|
||||
ntxId={noteContext.ntxId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Note paths
|
||||
function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
|
||||
return (
|
||||
<StatusBarDropdown
|
||||
title={t("status_bar.note_paths_title")}
|
||||
dropdownContainerClassName="dropdown-note-paths"
|
||||
icon="bx bx-directions"
|
||||
text={sortedNotePaths?.length}
|
||||
>
|
||||
<NotePathsWidget
|
||||
sortedNotePaths={sortedNotePaths}
|
||||
currentNotePath={notePath}
|
||||
/>
|
||||
</StatusBarDropdown>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Code note switcher
|
||||
function CodeNoteSwitcher({ note }: StatusBarContext) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const currentNoteMime = useNoteProperty(note, "mime");
|
||||
const mimeTypes = useMimeTypes();
|
||||
const correspondingMimeType = useMemo(() => (
|
||||
mimeTypes.find(m => m.mime === currentNoteMime)
|
||||
), [ mimeTypes, currentNoteMime ]);
|
||||
|
||||
return (note.type === "code" &&
|
||||
<>
|
||||
<StatusBarDropdown
|
||||
icon="bx bx-code-curly"
|
||||
text={correspondingMimeType?.title}
|
||||
title={t("status_bar.code_note_switcher")}
|
||||
dropdownContainerClassName="dropdown-code-note-switcher"
|
||||
>
|
||||
<NoteTypeCodeNoteList
|
||||
currentMimeType={currentNoteMime}
|
||||
mimeTypes={mimeTypes}
|
||||
changeNoteType={(type, mime) => server.put(`notes/${note.noteId}/type`, { type, mime })}
|
||||
setModalShown={() => setModalShown(true)}
|
||||
/>
|
||||
</StatusBarDropdown>
|
||||
{createPortal(
|
||||
<NoteTypeOptionsModal modalShown={modalShown} setModalShown={setModalShown} />,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
220
apps/client/src/widgets/note_bars/CollectionProperties.tsx
Normal file
220
apps/client/src/widgets/note_bars/CollectionProperties.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { t } from "i18next";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
||||
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
|
||||
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
grid: "bx bxs-grid",
|
||||
list: "bx bx-list-ul",
|
||||
calendar: "bx bx-calendar",
|
||||
table: "bx bx-table",
|
||||
geoMap: "bx bx-map-alt",
|
||||
board: "bx bx-columns",
|
||||
presentation: "bx bx-rectangle"
|
||||
};
|
||||
|
||||
export default function CollectionProperties({ note }: { note: FNote }) {
|
||||
const [ viewType, setViewType ] = useViewType(note);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
|
||||
<ViewOptions note={note} viewType={viewType} />
|
||||
<div className="spacer" />
|
||||
<HelpButton note={note} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewTypeSwitcher({ viewType, setViewType }: { viewType: ViewTypeOptions, setViewType: (newValue: ViewTypeOptions) => void }) {
|
||||
return (
|
||||
<Dropdown
|
||||
text={<>
|
||||
<Icon icon={ICON_MAPPINGS[viewType]} />
|
||||
{VIEW_TYPE_MAPPINGS[viewType]}
|
||||
</>}
|
||||
>
|
||||
{Object.entries(VIEW_TYPE_MAPPINGS).map(([ key, label ]) => (
|
||||
<FormListItem
|
||||
key={key}
|
||||
onClick={() => setViewType(key as ViewTypeOptions)}
|
||||
selected={viewType === key}
|
||||
disabled={viewType === key}
|
||||
icon={ICON_MAPPINGS[key as ViewTypeOptions]}
|
||||
>{label}</FormListItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOptions }) {
|
||||
const properties = bookPropertiesConfig[viewType].properties;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName="bx bx-cog icon-action"
|
||||
hideToggleArrow
|
||||
>
|
||||
{properties.map(property => (
|
||||
<ViewProperty key={property.label} note={note} property={property} />
|
||||
))}
|
||||
{properties.length > 0 && <FormDropdownDivider />}
|
||||
|
||||
<ViewProperty note={note} property={{
|
||||
type: "checkbox",
|
||||
icon: "bx bx-archive",
|
||||
label: t("book_properties.include_archived_notes"),
|
||||
bindToLabel: "includeArchived"
|
||||
} as CheckBoxProperty} />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
|
||||
switch (property.type) {
|
||||
case "button":
|
||||
return <ButtonPropertyView note={note} property={property} />;
|
||||
case "split-button":
|
||||
return <SplitButtonPropertyView note={note} property={property} />;
|
||||
case "checkbox":
|
||||
return <CheckBoxPropertyView note={note} property={property} />;
|
||||
case "number":
|
||||
return <NumberPropertyView note={note} property={property} />;
|
||||
case "combobox":
|
||||
return <ComboBoxPropertyView note={note} property={property} />;
|
||||
}
|
||||
}
|
||||
|
||||
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
icon={property.icon}
|
||||
title={property.title}
|
||||
onClick={() => {
|
||||
if (!parentComponent) return;
|
||||
property.onClick({
|
||||
note,
|
||||
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
|
||||
});
|
||||
}}
|
||||
>{property.label}</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const ItemsComponent = property.items;
|
||||
const clickContext = parentComponent && {
|
||||
note,
|
||||
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
|
||||
};
|
||||
|
||||
return (parentComponent &&
|
||||
<FormDropdownSubmenu
|
||||
icon={property.icon ?? "bx bx-empty"}
|
||||
title={property.label}
|
||||
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
|
||||
>
|
||||
<ItemsComponent note={note} parentComponent={parentComponent} />
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
|
||||
//@ts-expect-error Interop with text box which takes in string values even for numbers.
|
||||
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
|
||||
const disabled = property.disabled?.(note);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
icon={property.icon}
|
||||
disabled={disabled}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{property.label}
|
||||
<FormTextBox
|
||||
type="number"
|
||||
currentValue={value ?? ""} onChange={setValue}
|
||||
style={{ width: (property.width ?? 100) }}
|
||||
min={property.min ?? 0}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
|
||||
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
|
||||
|
||||
function renderItem(option: ComboBoxItem) {
|
||||
return (
|
||||
<FormListItem
|
||||
key={option.value}
|
||||
checked={value === option.value}
|
||||
onClick={() => setValue(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu
|
||||
title={property.label}
|
||||
icon={property.icon ?? "bx bx-empty"}
|
||||
>
|
||||
{(property.options).map((option, index) => {
|
||||
if ("items" in option) {
|
||||
return (
|
||||
<Fragment key={option.title}>
|
||||
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
|
||||
{option.items.map(renderItem)}
|
||||
{index < property.options.length - 1 && <FormDropdownDivider />}
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return renderItem(option);
|
||||
}
|
||||
})}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
|
||||
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
|
||||
return (
|
||||
<FormListToggleableItem
|
||||
icon={property.icon}
|
||||
title={property.label}
|
||||
currentValue={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function HelpButton({ note }: { note: FNote }) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
|
||||
return (helpUrl && (
|
||||
<ActionButton
|
||||
icon="bx bx-help-circle"
|
||||
onClick={(() => openInAppHelpFromUrl(helpUrl))}
|
||||
text={t("help-button.title")}
|
||||
/>
|
||||
));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
.note-icon-widget {
|
||||
padding-inline-start: 7px;
|
||||
padding-inline-start: 10px;
|
||||
margin-inline-end: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
@@ -13,7 +13,7 @@
|
||||
cursor: pointer;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
|
||||
.note-icon-widget button.note-icon:disabled {
|
||||
cursor: default;
|
||||
opacity: .75;
|
||||
@@ -68,4 +68,4 @@
|
||||
border: 1px dashed var(--muted-text-color);
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,46 @@ body.mobile .note-title-widget input.note-title {
|
||||
|
||||
body.desktop .note-title-widget input.note-title {
|
||||
font-size: 180%;
|
||||
}
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout {
|
||||
.title-row {
|
||||
container-type: size;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
transition: border 400ms ease-out;
|
||||
|
||||
&.hide-title {
|
||||
border-bottom-color: transparent;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@container (max-width: 700px) {
|
||||
.note-icon-widget .note-icon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.note-title-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.note-title {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.note-title-widget:focus-within + .note-badges,
|
||||
.ext-badge .text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-title-widget {
|
||||
input.note-title {
|
||||
--input-focus-background: transparent;
|
||||
--input-focus-outline-color: transparent;
|
||||
--input-hover-background: transparent;
|
||||
--input-hover-color: initial;
|
||||
--input-focus-color: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +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.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
|
||||
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
|
||||
this.$widget.toggleClass("protected", note.isProtected);
|
||||
|
||||
|
||||
49
apps/client/src/widgets/react/Badge.css
Normal file
49
apps/client/src/widgets/react/Badge.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.ext-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--badge-radius);
|
||||
font-size: 0.75em;
|
||||
background-color: var(--color, transparent);
|
||||
color: white;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-badge {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: var(--badge-radius);
|
||||
|
||||
.ext-badge {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,78 @@
|
||||
interface BadgeProps {
|
||||
import "./Badge.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren, MouseEventHandler } from "preact";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import Dropdown, { DropdownProps } from "./Dropdown";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface SimpleBadgeProps {
|
||||
className?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function Badge({ title, className }: BadgeProps) {
|
||||
return <span class={`badge ${className ?? ""}`}>{title}</span>
|
||||
}
|
||||
interface BadgeProps {
|
||||
text?: string;
|
||||
icon?: string;
|
||||
className?: string;
|
||||
tooltip?: string;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export default function SimpleBadge({ title, className }: SimpleBadgeProps) {
|
||||
return <span class={`badge ${className ?? ""}`}>{title}</span>;
|
||||
}
|
||||
|
||||
export function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useStaticTooltip(containerRef, {
|
||||
placement: "bottom",
|
||||
fallbackPlacements: [ "bottom" ],
|
||||
animation: false,
|
||||
html: true,
|
||||
title: tooltip
|
||||
});
|
||||
|
||||
const content = <>
|
||||
{icon && <><Icon icon={icon} /> </>}
|
||||
<span class="text">{text}</span>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx("ext-badge", className, { "clickable": !!onClick })}
|
||||
onClick={onClick}
|
||||
>
|
||||
{href ? <a href={href}>{content}</a> : <span>{content}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & {
|
||||
children: ComponentChildren,
|
||||
dropdownOptions?: Partial<DropdownProps>
|
||||
}) {
|
||||
return (
|
||||
<Dropdown
|
||||
className={`dropdown-badge dropdown-${className}`}
|
||||
text={<Badge className={className} {...props} />}
|
||||
noDropdownListStyle
|
||||
noSelectButtonStyle
|
||||
hideToggleArrow
|
||||
title={tooltip}
|
||||
titlePosition="bottom"
|
||||
{...dropdownOptions}
|
||||
dropdownOptions={{
|
||||
...dropdownOptions?.dropdownOptions,
|
||||
popperConfig: {
|
||||
...dropdownOptions?.dropdownOptions?.popperConfig,
|
||||
placement: "bottom", strategy: "fixed"
|
||||
}
|
||||
}}
|
||||
>{children}</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,8 +117,8 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
aria-expanded="false"
|
||||
id={id ?? ariaId}
|
||||
disabled={disabled}
|
||||
onMouseOver={() => showTooltip()}
|
||||
onMouseLeave={() => hideTooltip()}
|
||||
onMouseEnter={showTooltip}
|
||||
onMouseLeave={hideTooltip}
|
||||
{...buttonProps}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
.dropdown-item .description {
|
||||
font-size: small;
|
||||
color: var(--muted-text-color);
|
||||
white-space: normal;
|
||||
}
|
||||
.dropdown-item {
|
||||
.description {
|
||||
font-size: small;
|
||||
color: var(--muted-text-color);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dropdown-item span.bx {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
span.bx {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.switch-widget {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
--switch-track-width: 40px;
|
||||
--switch-track-height: 20px;
|
||||
--switch-thumb-width: 12px;
|
||||
--switch-thumb-height: var(--switch-thumb-width);
|
||||
|
||||
.contextual-help {
|
||||
margin-inline-start: 0.25em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact
|
||||
import "./FormList.css";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
import { handleRightToLeftPlacement, isMobile } from "../../services/utils";
|
||||
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
|
||||
import clsx from "clsx";
|
||||
import FormToggle from "./FormToggle";
|
||||
|
||||
interface FormListOpts {
|
||||
children: ComponentChildren;
|
||||
@@ -94,12 +95,13 @@ interface FormListItemOpts {
|
||||
description?: string;
|
||||
className?: string;
|
||||
rtl?: boolean;
|
||||
postContent?: ComponentChildren;
|
||||
}
|
||||
|
||||
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
|
||||
placement: handleRightToLeftPlacement("right"),
|
||||
fallbackPlacements: [ handleRightToLeftPlacement("right") ]
|
||||
}
|
||||
};
|
||||
|
||||
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
|
||||
const itemRef = useRef<HTMLLIElement>(null);
|
||||
@@ -132,6 +134,49 @@ export function FormListItem({ className, icon, value, title, active, disabled,
|
||||
);
|
||||
}
|
||||
|
||||
export function FormListToggleableItem({ title, currentValue, onChange, disabled, helpPage, ...props }: Omit<FormListItemOpts, "onClick" | "children"> & {
|
||||
title: string;
|
||||
currentValue: boolean;
|
||||
helpPage?: string;
|
||||
onChange(newValue: boolean): void | Promise<void>;
|
||||
}) {
|
||||
const isWaiting = useRef(false);
|
||||
|
||||
return (
|
||||
<FormListItem
|
||||
{...props}
|
||||
disabled={disabled}
|
||||
onClick={async (e) => {
|
||||
if ((e.target as HTMLElement | null)?.classList.contains("contextual-help")) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
if (!disabled && !isWaiting.current) {
|
||||
isWaiting.current = true;
|
||||
await onChange(!currentValue);
|
||||
isWaiting.current = false;
|
||||
}
|
||||
}}>
|
||||
<FormToggle
|
||||
switchOnName={title}
|
||||
switchOffName={title}
|
||||
currentValue={currentValue}
|
||||
onChange={() => {}}
|
||||
afterName={<>
|
||||
{helpPage && (
|
||||
<span
|
||||
class="bx bx-help-circle contextual-help"
|
||||
onClick={() => openInAppHelpFromUrl(helpPage)}
|
||||
/>
|
||||
)}
|
||||
<span class="switch-spacer" />
|
||||
</>}
|
||||
/>
|
||||
</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function FormListContent({ children, badges, description, disabled, disabledTooltip }: Pick<FormListItemOpts, "children" | "badges" | "description" | "disabled" | "disabledTooltip">) {
|
||||
return <>
|
||||
{children}
|
||||
@@ -139,7 +184,7 @@ function FormListContent({ children, badges, description, disabled, disabledTool
|
||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||
))}
|
||||
{disabled && disabledTooltip && (
|
||||
<span class="bx bx-info-circle disabled-tooltip" title={disabledTooltip} />
|
||||
<span class="bx bx-info-circle contextual-help" title={disabledTooltip} />
|
||||
)}
|
||||
{description && <div className="description">{description}</div>}
|
||||
</>;
|
||||
@@ -161,11 +206,17 @@ export function FormDropdownDivider() {
|
||||
return <div className="dropdown-divider" />;
|
||||
}
|
||||
|
||||
export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
|
||||
export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdownToggleClicked }: {
|
||||
icon: string,
|
||||
title: ComponentChildren,
|
||||
children: ComponentChildren,
|
||||
onDropdownToggleClicked?: () => void,
|
||||
dropStart?: boolean
|
||||
}) {
|
||||
const [ openOnMobile, setOpenOnMobile ] = useState(false);
|
||||
|
||||
return (
|
||||
<li className={`dropdown-item dropdown-submenu ${openOnMobile ? "submenu-open" : ""}`}>
|
||||
<li className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}>
|
||||
<span
|
||||
className="dropdown-toggle"
|
||||
onClick={(e) => {
|
||||
@@ -174,6 +225,10 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t
|
||||
if (isMobile()) {
|
||||
setOpenOnMobile(!openOnMobile);
|
||||
}
|
||||
|
||||
if (onDropdownToggleClicked) {
|
||||
onDropdownToggleClicked();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} />{" "}
|
||||
@@ -184,5 +239,5 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t
|
||||
{children}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
border-radius: 24px;
|
||||
background-color: var(--switch-off-track-background);
|
||||
transition: background 200ms ease-in;
|
||||
|
||||
&.disable-transitions {
|
||||
transition: none !important;
|
||||
|
||||
&:after {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch-widget .switch-button.on {
|
||||
@@ -103,4 +111,4 @@ body[dir=rtl] .switch-widget .switch-button.on:after {
|
||||
|
||||
.switch-widget .switch-help-button:hover {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
import clsx from "clsx";
|
||||
import "./FormToggle.css";
|
||||
import HelpButton from "./HelpButton";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
interface FormToggleProps {
|
||||
currentValue: boolean | null;
|
||||
onChange(newValue: boolean): void;
|
||||
switchOnName: string;
|
||||
switchOnTooltip: string;
|
||||
switchOnTooltip?: string;
|
||||
switchOffName: string;
|
||||
switchOffTooltip: string;
|
||||
switchOffTooltip?: string;
|
||||
helpPage?: string;
|
||||
disabled?: boolean;
|
||||
afterName?: ComponentChildren;
|
||||
}
|
||||
|
||||
export default function FormToggle({ currentValue, helpPage, switchOnName, switchOnTooltip, switchOffName, switchOffTooltip, onChange, disabled }: FormToggleProps) {
|
||||
export default function FormToggle({ currentValue, helpPage, switchOnName, switchOnTooltip, switchOffName, switchOffTooltip, onChange, disabled, afterName }: FormToggleProps) {
|
||||
const [ disableTransition, setDisableTransition ] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setDisableTransition(false);
|
||||
}, 100);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="switch-widget">
|
||||
<span className="switch-name">{ currentValue ? switchOffName : switchOnName }</span>
|
||||
{ afterName }
|
||||
|
||||
<label>
|
||||
<div
|
||||
className={`switch-button ${currentValue ? "on" : ""} ${disabled ? "disabled" : ""}`}
|
||||
className={clsx("switch-button", { "on": currentValue, disabled, "disable-transitions": disableTransition })}
|
||||
title={currentValue ? switchOffTooltip : switchOnTooltip }
|
||||
>
|
||||
<input
|
||||
@@ -37,5 +51,5 @@ export default function FormToggle({ currentValue, helpPage, switchOnName, switc
|
||||
|
||||
{ helpPage && <HelpButton className="switch-help-button" helpPage={helpPage} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean):
|
||||
return [
|
||||
(value === "true"),
|
||||
(newValue) => setValue(newValue ? "true" : "false")
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,17 +217,18 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb
|
||||
return [
|
||||
(parseInt(value, 10)),
|
||||
(newValue) => setValue(newValue)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
|
||||
*
|
||||
* @param name the name of the option to listen for.
|
||||
* @param needsRefresh whether to reload the frontend whenever the value is changed.
|
||||
* @returns an array where the first value is the current option value and the second value is the setter.
|
||||
*/
|
||||
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
|
||||
const [ value, setValue ] = useTriliumOption(name);
|
||||
export function useTriliumOptionJson<T>(name: OptionNames, needsRefresh?: boolean): [ T, (newValue: T) => Promise<void> ] {
|
||||
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
|
||||
useDebugValue(name);
|
||||
return [
|
||||
(JSON.parse(value) as T),
|
||||
@@ -315,7 +316,7 @@ export function useNoteContext() {
|
||||
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||
|
||||
return {
|
||||
note: note,
|
||||
note,
|
||||
noteId: noteContext?.note?.noteId,
|
||||
notePath: noteContext?.notePath,
|
||||
hoistedNoteId: noteContext?.hoistedNoteId,
|
||||
@@ -326,7 +327,65 @@ export function useNoteContext() {
|
||||
parentComponent,
|
||||
isReadOnlyTemporarilyDisabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useNoteContext}, but instead of using the note context from the split container that the component is part of, it uses the active note context instead
|
||||
* (the note currently focused by the user).
|
||||
*/
|
||||
export function useActiveNoteContext() {
|
||||
const [ noteContext, setNoteContext ] = useState<NoteContext | undefined>(appContext.tabManager.getActiveContext() ?? undefined);
|
||||
const [ notePath, setNotePath ] = useState<string | null | undefined>();
|
||||
const [ note, setNote ] = useState<FNote | null | undefined>();
|
||||
const [ , setViewScope ] = useState<ViewScope>();
|
||||
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
|
||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteContext) {
|
||||
setNoteContext(appContext.tabManager.getActiveContext() ?? undefined);
|
||||
}
|
||||
}, [ noteContext ]);
|
||||
|
||||
useEffect(() => {
|
||||
setNote(noteContext?.note);
|
||||
}, [ notePath ]);
|
||||
|
||||
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], () => {
|
||||
const noteContext = appContext.tabManager.getActiveContext() ?? undefined;
|
||||
setNoteContext(noteContext);
|
||||
setNotePath(noteContext?.notePath);
|
||||
setViewScope(noteContext?.viewScope);
|
||||
});
|
||||
useTriliumEvent("frocaReloaded", () => {
|
||||
setNote(noteContext?.note);
|
||||
});
|
||||
useTriliumEvent("noteTypeMimeChanged", ({ noteId }) => {
|
||||
if (noteId === note?.noteId) {
|
||||
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}`);
|
||||
|
||||
return {
|
||||
note,
|
||||
noteId: noteContext?.note?.noteId,
|
||||
notePath: noteContext?.notePath,
|
||||
hoistedNoteId: noteContext?.hoistedNoteId,
|
||||
ntxId: noteContext?.ntxId,
|
||||
viewScope: noteContext?.viewScope,
|
||||
componentId: parentComponent.componentId,
|
||||
noteContext,
|
||||
parentComponent,
|
||||
isReadOnlyTemporarilyDisabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -844,10 +903,12 @@ export function useGlobalShortcut(keyboardShortcut: string | null | undefined, h
|
||||
*/
|
||||
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
|
||||
const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined);
|
||||
const [ readOnlyAttr ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const [ autoReadOnlyDisabledAttr ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
|
||||
|
||||
const enableEditing = useCallback(() => {
|
||||
const enableEditing = useCallback((enabled = true) => {
|
||||
if (noteContext?.viewScope) {
|
||||
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
|
||||
noteContext.viewScope.readOnlyTemporarilyDisabled = enabled;
|
||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
|
||||
}
|
||||
}, [noteContext]);
|
||||
@@ -858,11 +919,11 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
|
||||
setIsReadOnly(readOnly);
|
||||
});
|
||||
}
|
||||
}, [ note, noteContext, noteContext?.viewScope ]);
|
||||
}, [ note, noteContext, noteContext?.viewScope, readOnlyAttr, autoReadOnlyDisabledAttr ]);
|
||||
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
|
||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||
setIsReadOnly(false);
|
||||
setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -886,15 +947,42 @@ async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function useChildNotes(parentNoteId: string) {
|
||||
export function useChildNotes(parentNoteId: string | undefined) {
|
||||
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
const childNotes = await parentNote?.getChildNotes();
|
||||
let childNotes: FNote[] | undefined;
|
||||
if (parentNoteId) {
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
childNotes = await parentNote?.getChildNotes();
|
||||
}
|
||||
setChildNotes(childNotes ?? []);
|
||||
})();
|
||||
}, [ parentNoteId ]);
|
||||
}, [ parentNoteId ]);
|
||||
|
||||
return childNotes;
|
||||
}
|
||||
|
||||
export function useLauncherVisibility(launchNoteId: string) {
|
||||
const checkIfVisible = useCallback(() => {
|
||||
const note = froca.getNoteFromCache(launchNoteId);
|
||||
return note?.getParentBranches().some(branch =>
|
||||
[ "_lbVisibleLaunchers", "_lbMobileVisibleLaunchers" ].includes(branch.parentNoteId)) ?? false;
|
||||
}, [ launchNoteId ]);
|
||||
|
||||
const [ isVisible, setIsVisible ] = useState<boolean>(checkIfVisible());
|
||||
|
||||
// React to note not being available in the cache.
|
||||
useEffect(() => {
|
||||
froca.getNote(launchNoteId).then(() => setIsVisible(checkIfVisible()));
|
||||
}, [ launchNoteId, checkIfVisible ]);
|
||||
|
||||
// React to changes.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getBranchRows().some(branch => branch.noteId === launchNoteId)) {
|
||||
setIsVisible(checkIfVisible());
|
||||
}
|
||||
});
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function disposeReactWidget(container: Element) {
|
||||
render(null, container);
|
||||
}
|
||||
|
||||
export function joinElements(components: ComponentChild[] | undefined, separator = ", ") {
|
||||
export function joinElements(components: ComponentChild[] | undefined, separator: ComponentChild = ", ") {
|
||||
if (!components) return <></>;
|
||||
|
||||
const joinedComponents: ComponentChild[] = [];
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { NOTE_TYPES } from "../../services/note_types";
|
||||
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
|
||||
import { getAvailableLocales, t } from "../../services/i18n";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
|
||||
import mime_types from "../../services/mime_types";
|
||||
import { Locale, LOCALES, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
import dialog from "../../services/dialog";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import protected_session from "../../services/protected_session";
|
||||
import FormDropdownList from "../react/FormDropdownList";
|
||||
import toast from "../../services/toast";
|
||||
import branches from "../../services/branches";
|
||||
import dialog from "../../services/dialog";
|
||||
import { getAvailableLocales, t } from "../../services/i18n";
|
||||
import mime_types from "../../services/mime_types";
|
||||
import { NOTE_TYPES } from "../../services/note_types";
|
||||
import protected_session from "../../services/protected_session";
|
||||
import server from "../../services/server";
|
||||
import sync from "../../services/sync";
|
||||
import toast from "../../services/toast";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import FormDropdownList from "../react/FormDropdownList";
|
||||
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import HelpButton from "../react/HelpButton";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import { CodeMimeTypesList } from "../type_widgets/options/code_notes";
|
||||
import { ContentLanguagesList } from "../type_widgets/options/i18n";
|
||||
import { LocaleSelector } from "../type_widgets/options/components/LocaleSelector";
|
||||
import { ContentLanguagesList } from "../type_widgets/options/i18n";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
export default function BasicPropertiesTab({ note }: TabContext) {
|
||||
return (
|
||||
@@ -37,18 +40,40 @@ export default function BasicPropertiesTab({ note }: TabContext) {
|
||||
}
|
||||
|
||||
function NoteTypeWidget({ note }: { note?: FNote | null }) {
|
||||
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
|
||||
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
|
||||
const mimeTypes = useMemo(() => {
|
||||
mime_types.loadMimeTypes();
|
||||
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled)
|
||||
}, [ codeNotesMimeTypes ]);
|
||||
const notSelectableNoteTypes = useMemo(() => NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type), []);
|
||||
|
||||
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
|
||||
const currentNoteMime = useNoteProperty(note, "mime");
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="note-type-container">
|
||||
<span>{t("basic_properties.note_type")}:</span>
|
||||
<Dropdown
|
||||
dropdownContainerClassName="note-type-dropdown"
|
||||
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>}
|
||||
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")}
|
||||
>
|
||||
<NoteTypeDropdownContent currentNoteType={currentNoteType} currentNoteMime={currentNoteMime} note={note} setModalShown={setModalShown} />
|
||||
</Dropdown>
|
||||
|
||||
{createPortal(
|
||||
<NoteTypeOptionsModal modalShown={modalShown} setModalShown={setModalShown} />,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note, setModalShown, noCodeNotes }: {
|
||||
currentNoteType?: NoteType;
|
||||
currentNoteMime?: string | null;
|
||||
note?: FNote | null;
|
||||
setModalShown: Dispatch<StateUpdater<boolean>>;
|
||||
noCodeNotes?: boolean;
|
||||
}) {
|
||||
const mimeTypes = useMimeTypes();
|
||||
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
|
||||
const changeNoteType = useCallback(async (type: NoteType, mime?: string) => {
|
||||
if (!note || (type === currentNoteType && mime === currentNoteMime)) {
|
||||
return;
|
||||
@@ -68,71 +93,94 @@ function NoteTypeWidget({ note }: { note?: FNote | null }) {
|
||||
}, [ note, currentNoteType, currentNoteMime ]);
|
||||
|
||||
return (
|
||||
<div className="note-type-container">
|
||||
<span>{t("basic_properties.note_type")}:</span>
|
||||
<Dropdown
|
||||
dropdownContainerClassName="note-type-dropdown"
|
||||
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>}
|
||||
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")}
|
||||
>
|
||||
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
|
||||
const badges: FormListBadge[] = [];
|
||||
if (isNew) {
|
||||
badges.push({
|
||||
className: "new-note-type-badge",
|
||||
text: t("note_types.new-feature")
|
||||
});
|
||||
}
|
||||
if (isBeta) {
|
||||
badges.push({
|
||||
text: t("note_types.beta-feature")
|
||||
});
|
||||
}
|
||||
<>
|
||||
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
|
||||
const badges: FormListBadge[] = [];
|
||||
if (isNew) {
|
||||
badges.push({
|
||||
className: "new-note-type-badge",
|
||||
text: t("note_types.new-feature")
|
||||
});
|
||||
}
|
||||
if (isBeta) {
|
||||
badges.push({
|
||||
text: t("note_types.beta-feature")
|
||||
});
|
||||
}
|
||||
|
||||
const checked = (type === currentNoteType);
|
||||
if (type !== "code") {
|
||||
return (
|
||||
const checked = (type === currentNoteType);
|
||||
if (noCodeNotes || type !== "code") {
|
||||
return (
|
||||
<FormListItem
|
||||
checked={checked}
|
||||
badges={badges}
|
||||
onClick={() => changeNoteType(type, mime)}
|
||||
>{title}</FormListItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
checked={checked}
|
||||
badges={badges}
|
||||
onClick={() => changeNoteType(type, mime)}
|
||||
>{title}</FormListItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
checked={checked}
|
||||
disabled
|
||||
>
|
||||
<strong>{title}</strong>
|
||||
</FormListItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})}
|
||||
disabled
|
||||
>
|
||||
<strong>{title}</strong>
|
||||
</FormListItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{mimeTypes.map(({ title, mime }) => (
|
||||
<FormListItem onClick={() => changeNoteType("code", mime)}>
|
||||
{title}
|
||||
</FormListItem>
|
||||
))}
|
||||
{!noCodeNotes && <NoteTypeCodeNoteList mimeTypes={mimeTypes} changeNoteType={changeNoteType} setModalShown={setModalShown} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
<FormDropdownDivider />
|
||||
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
|
||||
</Dropdown>
|
||||
export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteType, setModalShown }: {
|
||||
currentMimeType?: string;
|
||||
mimeTypes: MimeType[];
|
||||
changeNoteType(type: NoteType, mime: string): void;
|
||||
setModalShown(shown: boolean): void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{mimeTypes.map(({ title, mime }) => (
|
||||
<FormListItem
|
||||
key={mime}
|
||||
checked={mime === currentMimeType}
|
||||
onClick={() => changeNoteType("code", mime)}
|
||||
>
|
||||
{title}
|
||||
</FormListItem>
|
||||
))}
|
||||
|
||||
<Modal
|
||||
className="code-mime-types-modal"
|
||||
title={t("code_mime_types.title")}
|
||||
show={modalShown} onHidden={() => setModalShown(false)}
|
||||
size="xl" scrollable
|
||||
>
|
||||
<CodeMimeTypesList />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
<FormDropdownDivider />
|
||||
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useMimeTypes() {
|
||||
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
|
||||
const mimeTypes = useMemo(() => {
|
||||
mime_types.loadMimeTypes();
|
||||
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled);
|
||||
}, [ codeNotesMimeTypes ]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return mimeTypes;
|
||||
}
|
||||
|
||||
export function NoteTypeOptionsModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
return (
|
||||
<Modal
|
||||
className="code-mime-types-modal"
|
||||
title={t("code_mime_types.title")}
|
||||
show={modalShown} onHidden={() => setModalShown(false)}
|
||||
size="xl" scrollable
|
||||
>
|
||||
<CodeMimeTypesList />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
|
||||
@@ -187,22 +235,11 @@ function EditabilitySelect({ note }: { note?: FNote | null }) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BookmarkSwitch({ note }: { note?: FNote | null }) {
|
||||
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false);
|
||||
const refreshState = useCallback(() => {
|
||||
const isBookmarked = note && !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
|
||||
setIsBookmarked(!!isBookmarked);
|
||||
}, [ note ]);
|
||||
|
||||
useEffect(() => refreshState(), [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
|
||||
refreshState();
|
||||
}
|
||||
});
|
||||
const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
|
||||
|
||||
return (
|
||||
<div className="bookmark-switch-container">
|
||||
@@ -210,18 +247,36 @@ function BookmarkSwitch({ note }: { note?: FNote | null }) {
|
||||
switchOnName={t("bookmark_switch.bookmark")} switchOnTooltip={t("bookmark_switch.bookmark_this_note")}
|
||||
switchOffName={t("bookmark_switch.bookmark")} switchOffTooltip={t("bookmark_switch.remove_bookmark")}
|
||||
currentValue={isBookmarked}
|
||||
onChange={async (shouldBookmark) => {
|
||||
if (!note) return;
|
||||
const resp = await server.put<ToggleInParentResponse>(`notes/${note.noteId}/toggle-in-parent/_lbBookmarks/${shouldBookmark}`);
|
||||
|
||||
if (!resp.success && "message" in resp) {
|
||||
toast.showError(resp.message);
|
||||
}
|
||||
}}
|
||||
onChange={setIsBookmarked}
|
||||
disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useNoteBookmarkState(note: FNote | null | undefined) {
|
||||
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false);
|
||||
const refreshState = useCallback(() => {
|
||||
const isBookmarked = note && !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
|
||||
setIsBookmarked(!!isBookmarked);
|
||||
}, [ note ]);
|
||||
|
||||
const changeHandler = useCallback(async (shouldBookmark: boolean) => {
|
||||
if (!note) return;
|
||||
const resp = await server.put<ToggleInParentResponse>(`notes/${note.noteId}/toggle-in-parent/_lbBookmarks/${shouldBookmark}`);
|
||||
|
||||
if (!resp.success && "message" in resp) {
|
||||
toast.showError(resp.message);
|
||||
}
|
||||
}, [ note ]);
|
||||
|
||||
useEffect(() => refreshState(), [ refreshState ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
|
||||
refreshState();
|
||||
}
|
||||
});
|
||||
return [ isBookmarked, changeHandler ] as const;
|
||||
}
|
||||
|
||||
function TemplateSwitch({ note }: { note?: FNote | null }) {
|
||||
@@ -237,16 +292,33 @@ function TemplateSwitch({ note }: { note?: FNote | null }) {
|
||||
currentValue={isTemplate} onChange={setIsTemplate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SharedSwitch({ note }: { note?: FNote | null }) {
|
||||
const [ isShared, switchShareState ] = useShareState(note);
|
||||
|
||||
return (
|
||||
<div className="shared-switch-container">
|
||||
<FormToggle
|
||||
currentValue={isShared}
|
||||
onChange={switchShareState}
|
||||
switchOnName={t("shared_switch.shared")} switchOnTooltip={t("shared_switch.toggle-on-title")}
|
||||
switchOffName={t("shared_switch.shared")} switchOffTooltip={t("shared_switch.toggle-off-title")}
|
||||
helpPage="R9pX4DGra2Vt"
|
||||
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useShareState(note: FNote | null | undefined) {
|
||||
const [ isShared, setIsShared ] = useState(false);
|
||||
const refreshState = useCallback(() => {
|
||||
setIsShared(!!note?.hasAncestor("_share"));
|
||||
}, [ note ]);
|
||||
|
||||
useEffect(() => refreshState(), [ note ]);
|
||||
useEffect(() => refreshState(), [ refreshState ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
|
||||
refreshState();
|
||||
@@ -271,63 +343,71 @@ function SharedSwitch({ note }: { note?: FNote | null }) {
|
||||
sync.syncNow(true);
|
||||
}, [ note ]);
|
||||
|
||||
return (
|
||||
<div className="shared-switch-container">
|
||||
<FormToggle
|
||||
currentValue={isShared}
|
||||
onChange={switchShareState}
|
||||
switchOnName={t("shared_switch.shared")} switchOnTooltip={t("shared_switch.toggle-on-title")}
|
||||
switchOffName={t("shared_switch.shared")} switchOffTooltip={t("shared_switch.toggle-off-title")}
|
||||
helpPage="R9pX4DGra2Vt"
|
||||
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
return [ isShared, switchShareState ] as const;
|
||||
}
|
||||
|
||||
function NoteLanguageSwitch({ note }: { note?: FNote | null }) {
|
||||
return (
|
||||
<div className="note-language-container">
|
||||
<span>{t("basic_properties.language")}:</span>
|
||||
|
||||
|
||||
<NoteLanguageSelector note={note} />
|
||||
<HelpButton helpPage="veGu4faJErEM" style={{ marginInlineStart: "4px" }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteLanguageSelector({ note }: { note: FNote | null | undefined }) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LocaleSelector
|
||||
locales={locales}
|
||||
defaultLocale={DEFAULT_LOCALE}
|
||||
currentValue={currentNoteLanguage} onChange={setCurrentNoteLanguage}
|
||||
extraChildren={<>
|
||||
<FormListItem
|
||||
onClick={() => setModalShown(true)}
|
||||
icon="bx bx-cog"
|
||||
>{t("note_language.configure-languages")}</FormListItem>
|
||||
</>}
|
||||
/>
|
||||
{createPortal(
|
||||
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguageSwitcher(note: FNote | null | undefined) {
|
||||
const [ languages ] = useTriliumOption("languages");
|
||||
const DEFAULT_LOCALE = {
|
||||
id: "",
|
||||
name: t("note_language.not_set")
|
||||
};
|
||||
const [ currentNoteLanguage, setCurrentNoteLanguage ] = useNoteLabel(note, "language");
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const locales = useMemo(() => {
|
||||
const enabledLanguages = JSON.parse(languages ?? "[]") as string[];
|
||||
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
|
||||
return filteredLanguages;
|
||||
}, [ languages ]);
|
||||
return { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage };
|
||||
}
|
||||
|
||||
export function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
return (
|
||||
<div className="note-language-container">
|
||||
<span>{t("basic_properties.language")}:</span>
|
||||
|
||||
<LocaleSelector
|
||||
locales={locales}
|
||||
defaultLocale={DEFAULT_LOCALE}
|
||||
currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage}
|
||||
extraChildren={(
|
||||
<FormListItem
|
||||
onClick={() => setModalShown(true)}
|
||||
icon="bx bx-cog"
|
||||
>{t("note_language.configure-languages")}</FormListItem>
|
||||
)}
|
||||
>
|
||||
|
||||
</LocaleSelector>
|
||||
|
||||
<HelpButton helpPage="B0lcI9xz1r8K" style={{ marginInlineStart: "4px" }} />
|
||||
|
||||
<Modal
|
||||
className="content-languages-modal"
|
||||
title={t("content_language.title")}
|
||||
show={modalShown} onHidden={() => setModalShown(false)}
|
||||
size="lg" scrollable
|
||||
>
|
||||
<ContentLanguagesList />
|
||||
</Modal>
|
||||
</div>
|
||||
<Modal
|
||||
className="content-languages-modal"
|
||||
title={t("content_language.title")}
|
||||
show={modalShown} onHidden={() => setModalShown(false)}
|
||||
size="lg" scrollable
|
||||
>
|
||||
<ContentLanguagesList />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,34 +12,41 @@ import FormCheckbox from "../react/FormCheckbox";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
|
||||
const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
grid: t("book_properties.grid"),
|
||||
list: t("book_properties.list"),
|
||||
calendar: t("book_properties.calendar"),
|
||||
table: t("book_properties.table"),
|
||||
geoMap: t("book_properties.geo-map"),
|
||||
board: t("book_properties.board"),
|
||||
presentation: t("book_properties.presentation")
|
||||
export const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
grid: t("book_properties.grid"),
|
||||
list: t("book_properties.list"),
|
||||
calendar: t("book_properties.calendar"),
|
||||
table: t("book_properties.table"),
|
||||
geoMap: t("book_properties.geo-map"),
|
||||
board: t("book_properties.board"),
|
||||
presentation: t("book_properties.presentation")
|
||||
};
|
||||
|
||||
export default function CollectionPropertiesTab({ note }: TabContext) {
|
||||
const [ viewType, setViewType ] = useNoteLabel(note, "viewType");
|
||||
const defaultViewType = (note?.type === "search" ? "list" : "grid");
|
||||
const viewTypeWithDefault = (viewType ?? defaultViewType) as ViewTypeOptions;
|
||||
const properties = bookPropertiesConfig[viewTypeWithDefault].properties;
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
return (
|
||||
<div className="book-properties-widget">
|
||||
{note && (
|
||||
<>
|
||||
<CollectionTypeSwitcher viewType={viewTypeWithDefault} setViewType={setViewType} />
|
||||
<BookProperties viewType={viewTypeWithDefault} note={note} properties={properties} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
export default function CollectionPropertiesTab({ note }: TabContext) {
|
||||
const [viewType, setViewType] = useViewType(note);
|
||||
const properties = bookPropertiesConfig[viewType].properties;
|
||||
|
||||
return (
|
||||
<div className="book-properties-widget">
|
||||
{note && (
|
||||
<>
|
||||
{!isNewLayout && <CollectionTypeSwitcher viewType={viewType} setViewType={setViewType} />}
|
||||
<BookProperties viewType={viewType} note={note} properties={properties} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useViewType(note: FNote | null | undefined) {
|
||||
const [ viewType, setViewType ] = useNoteLabel(note, "viewType");
|
||||
const defaultViewType = (note?.type === "search" ? "list" : "grid");
|
||||
const viewTypeWithDefault = (viewType ?? defaultViewType) as ViewTypeOptions;
|
||||
return [ viewTypeWithDefault, setViewType ] as const;
|
||||
}
|
||||
|
||||
function CollectionTypeSwitcher({ viewType, setViewType }: { viewType: string, setViewType: (newValue: string) => void }) {
|
||||
@@ -148,7 +155,7 @@ function NumberPropertyView({ note, property }: { note: FNote, property: NumberP
|
||||
<FormTextBox
|
||||
type="number"
|
||||
currentValue={value ?? ""} onChange={setValue}
|
||||
style={{ width: (property.width ?? 100) + "px" }}
|
||||
style={{ width: (property.width ?? 100) }}
|
||||
min={property.min ?? 0}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
151
apps/client/src/widgets/ribbon/FormattingToolbar.spec.ts
Normal file
151
apps/client/src/widgets/ribbon/FormattingToolbar.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import NoteContext from "../../components/note_context";
|
||||
import { ViewMode } from "../../services/link";
|
||||
import { randomString } from "../../services/utils";
|
||||
import { buildNote } from "../../test/easy-froca";
|
||||
import { getFormattingToolbarState } from "./FormattingToolbar";
|
||||
|
||||
interface NoteContextInfo {
|
||||
type: NoteType;
|
||||
viewScope?: ViewMode;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
describe("Formatting toolbar logic", () => {
|
||||
beforeAll(() => {
|
||||
vi.mock("../../services/tree.ts", () => ({
|
||||
default: {
|
||||
getActiveContextNotePath() {
|
||||
return "root";
|
||||
},
|
||||
resolveNotePath(inputNotePath: string) {
|
||||
return inputNotePath;
|
||||
},
|
||||
getNoteIdFromUrl(url) {
|
||||
return url.split("/").at(-1);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
buildNote({
|
||||
id: "root",
|
||||
title: "Root"
|
||||
});
|
||||
});
|
||||
|
||||
async function buildConfig(noteContextInfos: NoteContextInfo[], activeIndex: number = 0) {
|
||||
const noteContexts: NoteContext[] = [];
|
||||
for (const noteContextData of noteContextInfos) {
|
||||
const noteContext = new NoteContext(randomString(10));
|
||||
const note = buildNote({
|
||||
title: randomString(5),
|
||||
type: noteContextData.type
|
||||
});
|
||||
|
||||
noteContext.noteId = note.noteId;
|
||||
expect(noteContext.note).toBe(note);
|
||||
noteContext.viewScope = {
|
||||
viewMode: noteContextData.viewScope ?? "default"
|
||||
};
|
||||
noteContext.isReadOnly = async () => !!noteContextData.isReadOnly;
|
||||
noteContext.getSubContexts = () => [];
|
||||
noteContexts.push(noteContext);
|
||||
};
|
||||
|
||||
const mainNoteContext = noteContexts[0];
|
||||
for (const noteContext of noteContexts) {
|
||||
noteContext.getMainContext = () => mainNoteContext;
|
||||
}
|
||||
|
||||
mainNoteContext.getSubContexts = () => noteContexts;
|
||||
return noteContexts[activeIndex];
|
||||
}
|
||||
|
||||
async function testSplit(noteContextInfos: NoteContextInfo[], activeIndex: number = 0, editor = "ckeditor-classic") {
|
||||
const noteContext = await buildConfig(noteContextInfos, activeIndex);
|
||||
return await getFormattingToolbarState(noteContext, noteContext.note, editor);
|
||||
}
|
||||
|
||||
describe("Single split", () => {
|
||||
it("should be hidden for floating toolbar", async () => {
|
||||
expect(await testSplit([ { type: "text" } ], 0, "ckeditor-balloon")).toBe("hidden");
|
||||
});
|
||||
|
||||
it("should be visible for single text note", async () => {
|
||||
expect(await testSplit([ { type: "text" } ])).toBe("visible");
|
||||
});
|
||||
|
||||
it("should be hidden for read-only text note", async () => {
|
||||
expect(await testSplit([ { type: "text", isReadOnly: true } ])).toBe("hidden");
|
||||
});
|
||||
|
||||
it("should be hidden for non-text note", async () => {
|
||||
expect(await testSplit([ { type: "code" } ])).toBe("hidden");
|
||||
});
|
||||
|
||||
it("should be hidden for wrong view mode", async () => {
|
||||
expect(await testSplit([ { type: "text", viewScope: "attachments" } ])).toBe("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Multi split", () => {
|
||||
it("should be hidden for floating toolbar", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "text" },
|
||||
{ type: "text" },
|
||||
], 0, "ckeditor-balloon")).toBe("hidden");
|
||||
});
|
||||
|
||||
it("should be visible for two text notes", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "text" },
|
||||
{ type: "text" },
|
||||
])).toBe("visible");
|
||||
});
|
||||
|
||||
it("should be disabled if on a non-text note", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "text" },
|
||||
{ type: "code" },
|
||||
], 1)).toBe("disabled");
|
||||
});
|
||||
|
||||
it("should be hidden for all non-text notes", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "code" },
|
||||
{ type: "canvas" },
|
||||
])).toBe("hidden");
|
||||
});
|
||||
|
||||
it("should be hidden for all read-only text notes", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "text", isReadOnly: true },
|
||||
{ type: "text", isReadOnly: true },
|
||||
])).toBe("hidden");
|
||||
});
|
||||
|
||||
it("should be visible for mixed view mode", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "text" },
|
||||
{ type: "text", viewScope: "attachments" }
|
||||
])).toBe("visible");
|
||||
});
|
||||
|
||||
it("should be hidden for all wrong view mode", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "text", viewScope: "attachments" },
|
||||
{ type: "text", viewScope: "attachments" }
|
||||
])).toBe("hidden");
|
||||
});
|
||||
|
||||
it("should be disabled for wrong view mode", async () => {
|
||||
expect(await testSplit([
|
||||
{ type: "text" },
|
||||
{ type: "text", viewScope: "attachments" }
|
||||
], 1)).toBe("disabled");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useRef } from "preact/hooks";
|
||||
import { useTriliumEvent, useTriliumOption } from "../react/hooks";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { useActiveNoteContext, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
/**
|
||||
@@ -33,5 +37,116 @@ export default function FormattingToolbar({ hidden, ntxId }: TabContext) {
|
||||
ref={containerRef}
|
||||
className={`classic-toolbar-widget ${hidden ? "hidden-ext" : ""}`}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const toolbarCache = new Map<string, HTMLElement | null | undefined>();
|
||||
|
||||
export function FixedFormattingToolbar() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { note, noteContext, ntxId } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const renderState = useRenderState(noteContext, note);
|
||||
const [ toolbarToRender, setToolbarToRender ] = useState<HTMLElement | null | undefined>();
|
||||
|
||||
// Populate the cache with the toolbar of every note context.
|
||||
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
|
||||
if (!eventNtxId) return;
|
||||
const toolbar = editor.ui.view.toolbar?.element;
|
||||
toolbarCache.set(eventNtxId, toolbar);
|
||||
// Replace on the spot if the editor crashed.
|
||||
if (eventNtxId === ntxId) {
|
||||
setToolbarToRender(toolbar);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean the cache when tabs are closed.
|
||||
useTriliumEvent("noteContextRemoved", ({ ntxIds: eventNtxIds }) => {
|
||||
for (const eventNtxId of eventNtxIds) {
|
||||
toolbarCache.delete(eventNtxId);
|
||||
}
|
||||
});
|
||||
|
||||
// Switch between the cached toolbar when user navigates to a different note context.
|
||||
useEffect(() => {
|
||||
if (!ntxId) return;
|
||||
const toolbar = toolbarCache.get(ntxId);
|
||||
if (toolbar) {
|
||||
setToolbarToRender(toolbar);
|
||||
}
|
||||
}, [ ntxId, noteType, noteContext ]);
|
||||
|
||||
// Render the toolbar.
|
||||
useEffect(() => {
|
||||
if (toolbarToRender) {
|
||||
containerRef.current?.replaceChildren(toolbarToRender);
|
||||
} else {
|
||||
containerRef.current?.replaceChildren();
|
||||
}
|
||||
}, [ toolbarToRender ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx("classic-toolbar-widget", {
|
||||
"hidden-ext": renderState === "hidden",
|
||||
"disabled": renderState === "disabled"
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useRenderState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined) {
|
||||
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
|
||||
const [ state, setState ] = useState("hidden");
|
||||
|
||||
useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ], () => {
|
||||
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getFormattingToolbarState(activeNoteContext, activeNote, textNoteEditorType).then(setState);
|
||||
}, [ activeNoteContext, activeNote, textNoteEditorType ]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export async function getFormattingToolbarState(activeNoteContext: NoteContext | undefined, activeNote: FNote | null | undefined, textNoteEditorType: string) {
|
||||
if (!activeNoteContext || textNoteEditorType !== "ckeditor-classic") {
|
||||
return "hidden";
|
||||
}
|
||||
|
||||
const subContexts = activeNoteContext?.getMainContext().getSubContexts() ?? [];
|
||||
if (subContexts.length === 1) {
|
||||
if (activeNote?.type !== "text" || activeNoteContext.viewScope?.viewMode !== "default") {
|
||||
return "hidden";
|
||||
}
|
||||
|
||||
const isReadOnly = await activeNoteContext.isReadOnly();
|
||||
if (isReadOnly) {
|
||||
return "hidden";
|
||||
}
|
||||
|
||||
return "visible";
|
||||
}
|
||||
|
||||
// If there are multiple note contexts (e.g. splits), the logic is slightly different.
|
||||
const textNoteContexts = subContexts.filter(s => s.note?.type === "text" && s.viewScope?.viewMode === "default");
|
||||
const textNoteContextsReadOnly = await Promise.all(textNoteContexts.map(sc => sc.isReadOnly()));
|
||||
|
||||
// If all text notes are hidden, no need to display the toolbar at all.
|
||||
if (textNoteContextsReadOnly.indexOf(false) === -1) {
|
||||
return "hidden";
|
||||
}
|
||||
|
||||
// If the current subcontext is not a text note, but there is at least an editable text then it must be disabled.
|
||||
if (activeNote?.type !== "text") return "disabled";
|
||||
|
||||
// If the current subcontext is a text note, it must not be read-only.
|
||||
const subContextIndex = textNoteContexts.indexOf(activeNoteContext);
|
||||
if (subContextIndex !== -1) {
|
||||
if (textNoteContextsReadOnly[subContextIndex]) return "disabled";
|
||||
}
|
||||
if (activeNoteContext.viewScope?.viewMode !== "default") return "disabled";
|
||||
return "visible";
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import RawHtml from "../react/RawHtml";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import AttributeDetailWidget from "../attribute_widgets/attribute_detail";
|
||||
|
||||
export default function InheritedAttributesTab({ note, componentId }: TabContext) {
|
||||
export default function InheritedAttributesTab({ note, componentId }: Pick<TabContext, "note" | "componentId">) {
|
||||
const [ inheritedAttributes, setInheritedAttributes ] = useState<FAttribute[]>();
|
||||
const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget());
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div className="inherited-attributes-widget">
|
||||
<div className="inherited-attributes-container selectable-text">
|
||||
@@ -83,4 +83,4 @@ function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onC
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,174 +1,269 @@
|
||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||
import { FormDropdownDivider, FormListHeader, 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 { useIsNoteReadOnly, useNoteLabel, useNoteProperty } from "../react/hooks";
|
||||
import { useTriliumOption } from "../react/hooks";
|
||||
import ActionButton from "../react/ActionButton"
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
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 { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
||||
import ws from "../../services/ws";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
|
||||
import protected_session from "../../services/protected_session";
|
||||
|
||||
interface NoteActionsProps {
|
||||
note?: FNote;
|
||||
noteContext?: NoteContext;
|
||||
}
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function NoteActions({ note, noteContext }: NoteActionsProps) {
|
||||
return (
|
||||
<>
|
||||
{note && <RevisionsButton note={note} />}
|
||||
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
|
||||
</>
|
||||
);
|
||||
export default function NoteActions() {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
return (
|
||||
<div className="ribbon-button-container" style={{ contain: "none" }}>
|
||||
{note && !isNewLayout && <RevisionsButton note={note} />}
|
||||
{note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionsButton({ note }: { note: FNote }) {
|
||||
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
|
||||
const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
|
||||
|
||||
return (isEnabled &&
|
||||
<ActionButton
|
||||
icon="bx bx-history"
|
||||
text={t("revisions_button.note_revisions")}
|
||||
triggerCommand="showRevisions"
|
||||
titlePosition="bottom"
|
||||
/>
|
||||
);
|
||||
return (isEnabled &&
|
||||
<ActionButton
|
||||
icon="bx bx-history"
|
||||
text={t("revisions_button.note_revisions")}
|
||||
triggerCommand="showRevisions"
|
||||
titlePosition="bottom"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const noteType = useNoteProperty(note, "type") ?? "";
|
||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
||||
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
|
||||
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
|
||||
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
|
||||
const isElectron = getIsElectron();
|
||||
const isMac = getIsMac();
|
||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
|
||||
const isSearchOrBook = ["search", "book"].includes(noteType);
|
||||
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
||||
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const noteType = useNoteProperty(note, "type") ?? "";
|
||||
const [viewType] = useNoteLabel(note, "viewType");
|
||||
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
|
||||
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
|
||||
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
|
||||
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
|
||||
const isElectron = getIsElectron();
|
||||
const isMac = getIsMac();
|
||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
|
||||
const isSearchOrBook = ["search", "book"].includes(noteType);
|
||||
const isHelpPage = note.noteId.startsWith("_help");
|
||||
const [syncServerHost] = useTriliumOption("syncServerHost");
|
||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||
const isNormalViewMode = noteContext?.viewScope?.viewMode === "default";
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName="bx bx-dots-vertical-rounded"
|
||||
className="note-actions"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
iconAction>
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
|
||||
className="note-actions"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
iconAction>
|
||||
|
||||
{isReadOnly && <>
|
||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||
command={() => enableEditing()} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
||||
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
|
||||
{isNewLayout && <CommandItem command="toggleRibbonTabNoteMap" icon="bx bxs-network-chart" disabled={isInOptionsOrHelp} text={t("note_actions.note_map")} />}
|
||||
|
||||
<FormDropdownDivider />
|
||||
|
||||
{isNewLayout && isNormalViewMode && !isHelpPage && <>
|
||||
<NoteBasicProperties note={note} />
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
||||
disabled={isInOptionsOrHelp || note.type === "search"}
|
||||
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
||||
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
|
||||
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
|
||||
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
|
||||
notePath: noteContext.notePath,
|
||||
defaultType: "single"
|
||||
})} />
|
||||
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
|
||||
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
|
||||
|
||||
<FormDropdownDivider />
|
||||
|
||||
<CommandItem command="showRevisions" icon="bx bx-history" text={t("note_actions.view_revisions")} />
|
||||
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
|
||||
|
||||
<FormDropdownDivider />
|
||||
|
||||
{canBeConvertedToAttachment && <ConvertToAttachment note={note} />}
|
||||
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")}
|
||||
/>}
|
||||
|
||||
<FormDropdownSubmenu icon="bx bx-wrench" title={t("note_actions.advanced")} dropStart>
|
||||
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
|
||||
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
|
||||
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
|
||||
{(syncServerHost && isElectron) &&
|
||||
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
|
||||
}
|
||||
|
||||
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
|
||||
</FormDropdownSubmenu>
|
||||
|
||||
<FormDropdownDivider />
|
||||
|
||||
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
|
||||
disabled={isInOptionsOrHelp}
|
||||
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteBasicProperties({ note }: { note: FNote }) {
|
||||
const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
|
||||
const [ isShared, switchShareState ] = useShareState(note);
|
||||
const [ isTemplate, setIsTemplate ] = useNoteLabelBoolean(note, "template");
|
||||
const isProtected = useNoteProperty(note, "isProtected");
|
||||
|
||||
return <>
|
||||
<FormListToggleableItem
|
||||
icon="bx bx-share-alt"
|
||||
title={t("shared_switch.shared")}
|
||||
currentValue={isShared} onChange={switchShareState}
|
||||
helpPage="R9pX4DGra2Vt"
|
||||
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
|
||||
/>
|
||||
<FormListToggleableItem
|
||||
icon="bx bx-lock-alt"
|
||||
title={t("protect_note.toggle-on")}
|
||||
currentValue={!!isProtected} onChange={shouldProtect => protected_session.protectNote(note.noteId, shouldProtect, false)}
|
||||
/>
|
||||
<FormListToggleableItem
|
||||
icon="bx bx-bookmark"
|
||||
title={t("bookmark_switch.bookmark")}
|
||||
currentValue={isBookmarked} onChange={setIsBookmarked}
|
||||
disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
|
||||
/>
|
||||
|
||||
{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")} />
|
||||
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
|
||||
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
|
||||
<FormDropdownDivider />
|
||||
<NoteTypeDropdown note={note} />
|
||||
<EditabilityDropdown note={note} />
|
||||
|
||||
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
|
||||
disabled={isInOptionsOrHelp || note.type === "search"}
|
||||
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
|
||||
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
|
||||
disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
|
||||
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
|
||||
notePath: noteContext.notePath,
|
||||
defaultType: "single"
|
||||
})} />
|
||||
<FormDropdownDivider />
|
||||
<FormListToggleableItem
|
||||
icon="bx bx-copy-alt"
|
||||
title={t("template_switch.template")}
|
||||
currentValue={isTemplate} onChange={setIsTemplate}
|
||||
helpPage="KC1HB96bqqHX"
|
||||
disabled={note?.noteId.startsWith("_options")}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
|
||||
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
|
||||
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
|
||||
{(syncServerHost && isElectron) &&
|
||||
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
|
||||
}
|
||||
<FormDropdownDivider />
|
||||
function EditabilityDropdown({ note }: { note: FNote }) {
|
||||
const [ readOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const [ autoReadOnlyDisabled, setAutoReadOnlyDisabled ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
|
||||
|
||||
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
|
||||
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
|
||||
disabled={isInOptionsOrHelp}
|
||||
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
|
||||
/>
|
||||
<FormDropdownDivider />
|
||||
function setState(readOnly: boolean, autoReadOnlyDisabled: boolean) {
|
||||
setReadOnly(readOnly);
|
||||
setAutoReadOnlyDisabled(autoReadOnlyDisabled);
|
||||
}
|
||||
|
||||
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
|
||||
{glob.isDev && <DevelopmentActions note={note} noteContext={noteContext} />}
|
||||
</Dropdown>
|
||||
);
|
||||
return (
|
||||
<FormDropdownSubmenu title={t("basic_properties.editable")} icon="bx bx-edit-alt" dropStart>
|
||||
<FormListItem checked={!readOnly && !autoReadOnlyDisabled} onClick={() => setState(false, false)} description={t("editability_select.note_is_editable")}>{t("editability_select.auto")}</FormListItem>
|
||||
<FormListItem checked={readOnly && !autoReadOnlyDisabled} onClick={() => setState(true, false)} description={t("editability_select.note_is_read_only")}>{t("editability_select.read_only")}</FormListItem>
|
||||
<FormListItem checked={!readOnly && autoReadOnlyDisabled} onClick={() => setState(false, true)} description={t("editability_select.note_is_always_editable")}>{t("editability_select.always_editable")}</FormListItem>
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteTypeDropdown({ note }: { note: FNote }) {
|
||||
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
|
||||
const currentNoteMime = useNoteProperty(note, "mime");
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu title={t("basic_properties.note_type")} icon="bx bx-file" dropStart>
|
||||
<NoteTypeDropdownContent
|
||||
currentNoteType={currentNoteType}
|
||||
currentNoteMime={currentNoteMime}
|
||||
note={note}
|
||||
setModalShown={() => { /* no-op since no code notes are displayed here */ }}
|
||||
noCodeNotes
|
||||
/>
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
return (
|
||||
<>
|
||||
<FormListHeader text="Development-only Actions" />
|
||||
<FormListHeader text="Development Actions" />
|
||||
<FormListItem
|
||||
icon="bx bx-printer"
|
||||
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
|
||||
>Open print page</FormListItem>
|
||||
{note.type === "text" && (
|
||||
<FormListItem
|
||||
icon="bx bx-error"
|
||||
onClick={() => {
|
||||
noteContext?.getTextEditor(editor => {
|
||||
editor.editing.view.change(() => {
|
||||
throw new Error("Editor crashed.");
|
||||
});
|
||||
<FormListItem
|
||||
icon="bx bx-error"
|
||||
disabled={note.type !== "text"}
|
||||
onClick={() => {
|
||||
noteContext?.getTextEditor(editor => {
|
||||
editor.editing.view.change(() => {
|
||||
throw new Error("Editor crashed.");
|
||||
});
|
||||
}}>Crash editor</FormListItem>)}
|
||||
});
|
||||
}}>Crash editor</FormListItem>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
triggerCommand={typeof command === "string" ? command : undefined}
|
||||
onClick={typeof command === "function" ? command : undefined}
|
||||
disabled={disabled}
|
||||
>{text}</FormListItem>
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
triggerCommand={typeof command === "string" ? command : undefined}
|
||||
onClick={typeof command === "function" ? command : undefined}
|
||||
disabled={disabled}
|
||||
>{text}</FormListItem>;
|
||||
}
|
||||
|
||||
function ConvertToAttachment({ note }: { note: FNote }) {
|
||||
return (
|
||||
<FormListItem
|
||||
icon="bx bx-paperclip"
|
||||
onClick={async () => {
|
||||
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<FormListItem
|
||||
icon="bx bx-paperclip"
|
||||
onClick={async () => {
|
||||
if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
|
||||
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
|
||||
|
||||
if (!newAttachment) {
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
|
||||
return;
|
||||
}
|
||||
if (!newAttachment) {
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title }));
|
||||
return;
|
||||
}
|
||||
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
||||
viewScope: {
|
||||
viewMode: "attachments",
|
||||
attachmentId: newAttachment.attachmentId
|
||||
}
|
||||
});
|
||||
}}
|
||||
>{t("note_actions.convert_into_attachment")}</FormListItem>
|
||||
)
|
||||
toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
|
||||
viewScope: {
|
||||
viewMode: "attachments",
|
||||
attachmentId: newAttachment.attachmentId
|
||||
}
|
||||
});
|
||||
}}
|
||||
>{t("note_actions.convert_into_attachment")}</FormListItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { MetadataResponse, NoteSizeResponse, SubtreeSizeResponse } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
import Button from "../react/Button";
|
||||
@@ -8,12 +7,76 @@ import { formatDateTime } from "../../utils/formatters";
|
||||
import { formatSize } from "../../services/utils";
|
||||
import LoadingSpinner from "../react/LoadingSpinner";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import FNote from "../../entities/fnote";
|
||||
|
||||
export default function NoteInfoTab({ note }: TabContext) {
|
||||
const [ metadata, setMetadata ] = useState<MetadataResponse>();
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function NoteInfoTab({ note }: { note: FNote | null | undefined }) {
|
||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||
|
||||
return (
|
||||
<div className="note-info-widget">
|
||||
{note && (
|
||||
<>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.note_id")}:</span>
|
||||
<span className="note-info-id selectable-text">{note.noteId}</span>
|
||||
</div>
|
||||
{!isNewLayout && <div className="note-info-item">
|
||||
<span>{t("note_info_widget.created")}:</span>
|
||||
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
|
||||
</div>}
|
||||
{!isNewLayout && <div className="note-info-item">
|
||||
<span>{t("note_info_widget.modified")}:</span>
|
||||
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
|
||||
</div>}
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.type")}:</span>
|
||||
<span>
|
||||
<span className="note-info-type">{note.type}</span>{' '}
|
||||
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span>
|
||||
<span className="note-info-size-col-span">
|
||||
<NoteSizeWidget {...sizeProps} />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteSizeWidget({ isLoading, noteSizeResponse, subtreeSizeResponse, requestSizeInfo }: Omit<ReturnType<typeof useNoteMetadata>, "metadata">) {
|
||||
return <>
|
||||
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
|
||||
<Button
|
||||
className="calculate-button"
|
||||
icon="bx bx-calculator"
|
||||
text={t("note_info_widget.calculate")}
|
||||
onClick={requestSizeInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="note-sizes-wrapper selectable-text">
|
||||
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
|
||||
{" "}
|
||||
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
|
||||
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
|
||||
}
|
||||
{isLoading && <LoadingSpinner />}
|
||||
</span>
|
||||
</>;
|
||||
}
|
||||
|
||||
export function useNoteMetadata(note: FNote | null | undefined) {
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ noteSizeResponse, setNoteSizeResponse ] = useState<NoteSizeResponse>();
|
||||
const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState<SubtreeSizeResponse>();
|
||||
const [ metadata, setMetadata ] = useState<MetadataResponse>();
|
||||
|
||||
function refresh() {
|
||||
if (note) {
|
||||
@@ -25,7 +88,20 @@ export default function NoteInfoTab({ note }: TabContext) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note?.noteId ]);
|
||||
function requestSizeInfo() {
|
||||
if (!note) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setTimeout(async () => {
|
||||
await Promise.allSettled([
|
||||
server.get<NoteSizeResponse>(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse),
|
||||
server.get<SubtreeSizeResponse>(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse)
|
||||
]);
|
||||
setIsLoading(false);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
const noteId = note?.noteId;
|
||||
if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) {
|
||||
@@ -33,62 +109,5 @@ export default function NoteInfoTab({ note }: TabContext) {
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="note-info-widget">
|
||||
{note && (
|
||||
<>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.note_id")}:</span>
|
||||
<span className="note-info-id selectable-text">{note.noteId}</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.created")}:</span>
|
||||
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.modified")}:</span>
|
||||
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span>{t("note_info_widget.type")}:</span>
|
||||
<span>
|
||||
<span className="note-info-type">{note.type}</span>{' '}
|
||||
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="note-info-item">
|
||||
<span title={t("note_info_widget.note_size_info")}>{t("note_info_widget.note_size")}:</span>
|
||||
<span className="note-info-size-col-span">
|
||||
{!isLoading && !noteSizeResponse && !subtreeSizeResponse && (
|
||||
<Button
|
||||
className="calculate-button"
|
||||
icon="bx bx-calculator"
|
||||
text={t("note_info_widget.calculate")}
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
setTimeout(async () => {
|
||||
await Promise.allSettled([
|
||||
server.get<NoteSizeResponse>(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse),
|
||||
server.get<SubtreeSizeResponse>(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse)
|
||||
]);
|
||||
setIsLoading(false);
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="note-sizes-wrapper selectable-text">
|
||||
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
|
||||
{" "}
|
||||
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&
|
||||
<span className="subtree-size">{t("note_info_widget.subtree_size", { size: formatSize(subtreeSizeResponse.subTreeSize), count: subtreeSizeResponse.subTreeNoteCount })}</span>
|
||||
}
|
||||
{isLoading && <LoadingSpinner />}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo };
|
||||
}
|
||||
|
||||
@@ -1,13 +1,50 @@
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import FNote, { NotePathRecord } from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||
import Button from "../react/Button";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { NotePathRecord } from "../../entities/fnote";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
return <NotePathsWidget sortedNotePaths={sortedNotePaths} currentNotePath={notePath} />;
|
||||
}
|
||||
|
||||
export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
|
||||
sortedNotePaths: NotePathRecord[] | undefined;
|
||||
currentNotePath?: string | null | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div class="note-paths-widget">
|
||||
<>
|
||||
<div className="note-path-intro">
|
||||
{sortedNotePaths?.length ? t("note_paths.intro_placed") : t("note_paths.intro_not_placed")}
|
||||
</div>
|
||||
|
||||
<ul className="note-path-list">
|
||||
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
|
||||
<NotePath
|
||||
key={sortedNotePath.notePath}
|
||||
currentNotePath={currentNotePath}
|
||||
notePathRecord={sortedNotePath}
|
||||
/>
|
||||
)) : undefined}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
triggerCommand="cloneNoteIdsTo"
|
||||
text={t("note_paths.clone_button")}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSortedNotePaths(note: FNote | null | undefined, hoistedNoteId?: string) {
|
||||
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
|
||||
|
||||
function refresh() {
|
||||
@@ -17,7 +54,7 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
|
||||
.filter((notePath) => !notePath.isHidden));
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note?.noteId ]);
|
||||
useEffect(refresh, [ note, hoistedNoteId ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
const noteId = note?.noteId;
|
||||
if (!noteId) return;
|
||||
@@ -27,35 +64,13 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="note-paths-widget">
|
||||
<>
|
||||
<div className="note-path-intro">
|
||||
{sortedNotePaths?.length ? t("note_paths.intro_placed") : t("note_paths.intro_not_placed")}
|
||||
</div>
|
||||
|
||||
<ul className="note-path-list">
|
||||
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
|
||||
<NotePath
|
||||
currentNotePath={notePath}
|
||||
notePathRecord={sortedNotePath}
|
||||
/>
|
||||
)) : undefined}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
triggerCommand="cloneNoteIdsTo"
|
||||
text={t("note_paths.clone_button")}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
return sortedNotePaths;
|
||||
}
|
||||
|
||||
function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: string | null, notePathRecord?: NotePathRecord }) {
|
||||
const notePath = notePathRecord?.notePath ?? [];
|
||||
const notePathString = useMemo(() => notePath.join("/"), [ notePath ]);
|
||||
|
||||
const notePath = notePathRecord?.notePath;
|
||||
const notePathString = useMemo(() => (notePath ?? []).join("/"), [ notePath ]);
|
||||
|
||||
const [ classes, icons ] = useMemo(() => {
|
||||
const classes: string[] = [];
|
||||
const icons: { icon: string, title: string }[] = [];
|
||||
@@ -67,17 +82,17 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
|
||||
classes.push("path-in-hoisted-subtree");
|
||||
} else {
|
||||
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") })
|
||||
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") });
|
||||
}
|
||||
|
||||
if (notePathRecord?.isArchived) {
|
||||
classes.push("path-archived");
|
||||
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") })
|
||||
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") });
|
||||
}
|
||||
|
||||
if (notePathRecord?.isSearch) {
|
||||
classes.push("path-search");
|
||||
icons.push({ icon: "bx bx-search", title: t("note_paths.search") })
|
||||
icons.push({ icon: "bx bx-search", title: t("note_paths.search") });
|
||||
}
|
||||
|
||||
return [ classes.join(" "), icons ];
|
||||
@@ -86,7 +101,7 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
// Determine the full note path (for the links) of every component of the current note path.
|
||||
const pathSegments: string[] = [];
|
||||
const fullNotePaths: string[] = [];
|
||||
for (const noteId of notePath) {
|
||||
for (const noteId of notePath ?? []) {
|
||||
pathSegments.push(noteId);
|
||||
fullNotePaths.push(pathSegments.join("/"));
|
||||
}
|
||||
@@ -94,12 +109,12 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
return (
|
||||
<li class={classes}>
|
||||
{joinElements(fullNotePaths.map(notePath => (
|
||||
<NoteLink notePath={notePath} noPreview />
|
||||
)), " / ")}
|
||||
<NoteLink key={notePath} notePath={notePath} noPreview />
|
||||
)), NOTE_PATH_TITLE_SEPARATOR)}
|
||||
|
||||
{icons.map(({ icon, title }) => (
|
||||
<span class={icon} title={title} />
|
||||
<span key={title} class={icon} title={title} />
|
||||
))}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useRef } from "preact/hooks";
|
||||
|
||||
import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks";
|
||||
import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
@@ -25,5 +26,5 @@ export default function OwnedAttributesTab({ note, hidden, activate, ntxId, ...r
|
||||
<AttributeEditor api={api} ntxId={ntxId} note={note} {...restProps} hidden={hidden} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||
import "./style.css";
|
||||
|
||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||
import { EventNames } from "../../components/app_context";
|
||||
import NoteActions from "./NoteActions";
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { EventNames } from "../../components/app_context";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
||||
|
||||
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
||||
|
||||
@@ -16,7 +17,9 @@ interface ComputedTab extends Indexed<TabConfiguration> {
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
export default function Ribbon() {
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function Ribbon({ children }: { children?: preact.ComponentChildren }) {
|
||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
||||
@@ -29,7 +32,8 @@ export default function Ribbon() {
|
||||
async function refresh() {
|
||||
const computedTabs: ComputedTab[] = [];
|
||||
for (const tab of TAB_CONFIGURATION) {
|
||||
const shouldShow = await shouldShowTab(tab.show, titleContext);
|
||||
const shouldAvoid = (isNewLayout && tab.avoidInNewLayout);
|
||||
const shouldShow = !shouldAvoid && await shouldShowTab(tab.show, titleContext);
|
||||
computedTabs.push({
|
||||
...tab,
|
||||
shouldShow: !!shouldShow
|
||||
@@ -54,7 +58,7 @@ export default function Ribbon() {
|
||||
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
|
||||
if (!computedTabs) return;
|
||||
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
|
||||
if (correspondingTab) {
|
||||
if (correspondingTab?.shouldShow) {
|
||||
if (activeTabIndex !== correspondingTab.index) {
|
||||
setActiveTabIndex(correspondingTab.index);
|
||||
} else {
|
||||
@@ -63,9 +67,10 @@ export default function Ribbon() {
|
||||
}
|
||||
}, [ computedTabs, activeTabIndex ]));
|
||||
|
||||
const shouldShowRibbon = (noteContext?.viewScope?.viewMode === "default" && !noteContext.noteId?.startsWith("_options"));
|
||||
return (
|
||||
<div
|
||||
className={clsx("ribbon-container", noteContext?.viewScope?.viewMode !== "default" && "hidden-ext")}
|
||||
className={clsx("ribbon-container", !shouldShowRibbon && "hidden-ext")}
|
||||
style={{ contain: "none" }}
|
||||
>
|
||||
<div className="ribbon-top-row">
|
||||
@@ -87,9 +92,7 @@ export default function Ribbon() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="ribbon-button-container">
|
||||
{ note && <NoteActions note={note} noteContext={noteContext} /> }
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="ribbon-body-container">
|
||||
@@ -120,7 +123,7 @@ export default function Ribbon() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
|
||||
@@ -143,7 +146,7 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
|
||||
|
||||
<div class="ribbon-tab-spacer" />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
import ScriptTab from "./ScriptTab";
|
||||
import EditedNotesTab from "./EditedNotesTab";
|
||||
import NotePropertiesTab from "./NotePropertiesTab";
|
||||
import NoteInfoTab from "./NoteInfoTab";
|
||||
import SimilarNotesTab from "./SimilarNotesTab";
|
||||
import FilePropertiesTab from "./FilePropertiesTab";
|
||||
import ImagePropertiesTab from "./ImagePropertiesTab";
|
||||
import NotePathsTab from "./NotePathsTab";
|
||||
import NoteMapTab from "./NoteMapTab";
|
||||
import OwnedAttributesTab from "./OwnedAttributesTab";
|
||||
import InheritedAttributesTab from "./InheritedAttributesTab";
|
||||
import CollectionPropertiesTab from "./CollectionPropertiesTab";
|
||||
import SearchDefinitionTab from "./SearchDefinitionTab";
|
||||
import BasicPropertiesTab from "./BasicPropertiesTab";
|
||||
import FormattingToolbar from "./FormattingToolbar";
|
||||
import options from "../../services/options";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import BasicPropertiesTab from "./BasicPropertiesTab";
|
||||
import CollectionPropertiesTab from "./CollectionPropertiesTab";
|
||||
import EditedNotesTab from "./EditedNotesTab";
|
||||
import FilePropertiesTab from "./FilePropertiesTab";
|
||||
import FormattingToolbar from "./FormattingToolbar";
|
||||
import ImagePropertiesTab from "./ImagePropertiesTab";
|
||||
import InheritedAttributesTab from "./InheritedAttributesTab";
|
||||
import NoteInfoTab from "./NoteInfoTab";
|
||||
import NoteMapTab from "./NoteMapTab";
|
||||
import NotePathsTab from "./NotePathsTab";
|
||||
import NotePropertiesTab from "./NotePropertiesTab";
|
||||
import OwnedAttributesTab from "./OwnedAttributesTab";
|
||||
import { TabConfiguration } from "./ribbon-interface";
|
||||
import ScriptTab from "./ScriptTab";
|
||||
import SearchDefinitionTab from "./SearchDefinitionTab";
|
||||
import SimilarNotesTab from "./SimilarNotesTab";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
{
|
||||
title: t("classic_editor_toolbar.title"),
|
||||
icon: "bx bx-text",
|
||||
show: async ({ note, noteContext }) => note?.type === "text"
|
||||
show: async ({ note, noteContext }) => note?.type === "text" && noteContext?.viewScope?.viewMode === "default"
|
||||
&& options.get("textNoteEditorType") === "ckeditor-classic"
|
||||
&& !(await noteContext?.isReadOnly()),
|
||||
toggleCommand: "toggleRibbonTabClassicEditor",
|
||||
content: FormattingToolbar,
|
||||
activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"),
|
||||
stayInDom: true
|
||||
stayInDom: !isNewLayout,
|
||||
avoidInNewLayout: true
|
||||
},
|
||||
{
|
||||
title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
|
||||
icon: "bx bx-play",
|
||||
content: ScriptTab,
|
||||
activate: true,
|
||||
show: ({ note }) => note &&
|
||||
show: ({ note }) => note && !isNewLayout &&
|
||||
(note.isTriliumScript() || note.isTriliumSqlite()) &&
|
||||
(note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
|
||||
},
|
||||
@@ -56,14 +60,14 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
title: t("book_properties.book_properties"),
|
||||
icon: "bx bx-book",
|
||||
content: CollectionPropertiesTab,
|
||||
show: ({ note }) => note?.type === "book" || note?.type === "search",
|
||||
show: ({ note }) => !isNewLayout && note?.type === "book" || note?.type === "search",
|
||||
toggleCommand: "toggleRibbonTabBookProperties"
|
||||
},
|
||||
{
|
||||
title: t("note_properties.info"),
|
||||
icon: "bx bx-info-square",
|
||||
content: NotePropertiesTab,
|
||||
show: ({ note }) => !!note?.getLabelValue("pageUrl"),
|
||||
show: ({ note }) => !isNewLayout && !!note?.getLabelValue("pageUrl"),
|
||||
activate: true
|
||||
},
|
||||
{
|
||||
@@ -83,53 +87,52 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
activate: true,
|
||||
},
|
||||
{
|
||||
// BasicProperties
|
||||
title: t("basic_properties.basic_properties"),
|
||||
icon: "bx bx-slider",
|
||||
content: BasicPropertiesTab,
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabBasicProperties"
|
||||
},
|
||||
{
|
||||
title: t("owned_attribute_list.owned_attributes"),
|
||||
icon: "bx bx-list-check",
|
||||
content: OwnedAttributesTab,
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabOwnedAttributes",
|
||||
stayInDom: true
|
||||
stayInDom: !isNewLayout
|
||||
},
|
||||
{
|
||||
title: t("inherited_attribute_list.title"),
|
||||
icon: "bx bx-list-plus",
|
||||
content: InheritedAttributesTab,
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabInheritedAttributes"
|
||||
},
|
||||
{
|
||||
title: t("note_paths.title"),
|
||||
icon: "bx bx-collection",
|
||||
content: NotePathsTab,
|
||||
show: true,
|
||||
show: !isNewLayout,
|
||||
toggleCommand: "toggleRibbonTabNotePaths"
|
||||
},
|
||||
{
|
||||
title: t("note_map.title"),
|
||||
icon: "bx bxs-network-chart",
|
||||
content: NoteMapTab,
|
||||
show: true,
|
||||
show: !isNewLayout,
|
||||
toggleCommand: "toggleRibbonTabNoteMap"
|
||||
},
|
||||
{
|
||||
title: t("similar_notes.title"),
|
||||
icon: "bx bx-bar-chart",
|
||||
show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
||||
show: ({ note }) => !isNewLayout && note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
||||
content: SimilarNotesTab,
|
||||
toggleCommand: "toggleRibbonTabSimilarNotes"
|
||||
},
|
||||
{
|
||||
title: t("note_info_widget.title"),
|
||||
icon: "bx bx-info-circle",
|
||||
show: ({ note }) => !!note,
|
||||
show: ({ note }) => !isNewLayout && !!note,
|
||||
content: NoteInfoTab,
|
||||
toggleCommand: "toggleRibbonTabNoteInfo"
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ interface BookConfig {
|
||||
export interface CheckBoxProperty {
|
||||
type: "checkbox",
|
||||
label: string;
|
||||
bindToLabel: FilterLabelsByType<boolean>
|
||||
bindToLabel: FilterLabelsByType<boolean>;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface ButtonProperty {
|
||||
@@ -40,10 +41,11 @@ export interface NumberProperty {
|
||||
bindToLabel: FilterLabelsByType<number>;
|
||||
width?: number;
|
||||
min?: number;
|
||||
icon?: string;
|
||||
disabled?: (note: FNote) => boolean;
|
||||
}
|
||||
|
||||
interface ComboBoxItem {
|
||||
export interface ComboBoxItem {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
@@ -56,6 +58,7 @@ interface ComboBoxGroup {
|
||||
export interface ComboBoxProperty {
|
||||
type: "combobox",
|
||||
label: string;
|
||||
icon?: string;
|
||||
bindToLabel: FilterLabelsByType<string>;
|
||||
/**
|
||||
* The default value is used when the label is not set.
|
||||
@@ -107,11 +110,13 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.hide-weekends"),
|
||||
icon: "bx bx-calendar-week",
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:hideWeekends"
|
||||
},
|
||||
{
|
||||
label: t("book_properties_config.display-week-numbers"),
|
||||
icon: "bx bx-hash",
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:weekNumbers"
|
||||
}
|
||||
@@ -121,6 +126,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.map-style"),
|
||||
icon: "bx bx-palette",
|
||||
type: "combobox",
|
||||
bindToLabel: "map:style",
|
||||
defaultValue: DEFAULT_MAP_LAYER_NAME,
|
||||
@@ -147,6 +153,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
},
|
||||
{
|
||||
label: t("book_properties_config.show-scale"),
|
||||
icon: "bx bx-ruler",
|
||||
type: "checkbox",
|
||||
bindToLabel: "map:scale"
|
||||
}
|
||||
@@ -156,6 +163,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.max-nesting-depth"),
|
||||
icon: "bx bx-subdirectory-right",
|
||||
type: "number",
|
||||
bindToLabel: "maxNestingDepth",
|
||||
width: 65,
|
||||
@@ -171,6 +179,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
{
|
||||
label: "Theme",
|
||||
type: "combobox",
|
||||
icon: "bx bx-palette",
|
||||
bindToLabel: "presentation:theme",
|
||||
defaultValue: DEFAULT_THEME,
|
||||
options: getPresentationThemes().map(theme => ({
|
||||
|
||||
@@ -30,4 +30,5 @@ export interface TabConfiguration {
|
||||
* By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed.
|
||||
*/
|
||||
stayInDom?: boolean;
|
||||
avoidInNewLayout?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
.ribbon-container {
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
/* When content header is floating, ribbon sticks below it */
|
||||
.scrolling-container:has(.content-header-widget.floating) .ribbon-container {
|
||||
position: sticky;
|
||||
top: var(--content-header-height, 100px);
|
||||
}
|
||||
|
||||
.ribbon-top-row {
|
||||
@@ -24,12 +32,15 @@
|
||||
max-width: max-content;
|
||||
flex-grow: 10;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.ribbon-tab-title .bx {
|
||||
font-size: 150%;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.ribbon-tab-title.active {
|
||||
@@ -71,12 +82,9 @@
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.ribbon-button-container > * {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
margin-inline-start: 10px;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ribbon-body {
|
||||
@@ -144,6 +152,15 @@
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .classic-toolbar-widget {
|
||||
transition: opacity 250ms ease-in;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Script Tab */
|
||||
@@ -347,6 +364,10 @@ body[dir=rtl] .attribute-list-editor {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
padding: 14px 12px 13px 12px;
|
||||
|
||||
a.reference-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -386,6 +407,8 @@ body[dir=rtl] .attribute-list-editor {
|
||||
.note-actions {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-menu {
|
||||
@@ -404,4 +427,29 @@ body[dir=rtl] .attribute-list-editor {
|
||||
background-color: transparent !important;
|
||||
pointer-events: none; /* makes it unclickable */
|
||||
}
|
||||
/* #endregion */
|
||||
/* #endregion */
|
||||
|
||||
/* #region Experimental layout */
|
||||
body.experimental-feature-new-layout {
|
||||
.ribbon-top-row {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ribbon-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
border: 0;
|
||||
|
||||
.ribbon-tab-spacer,
|
||||
.ribbon-tab-title,
|
||||
.ribbon-body {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon-button-container {
|
||||
border-bottom: 0 !important;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -10,8 +10,24 @@ import RawHtml from "./react/RawHtml";
|
||||
|
||||
export default function SharedInfo() {
|
||||
const { note } = useNoteContext();
|
||||
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
||||
const { isSharedExternally, link } = useShareInfo(note);
|
||||
|
||||
return (
|
||||
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
|
||||
{link && (
|
||||
<RawHtml html={isSharedExternally
|
||||
? t("shared_info.shared_publicly", { link })
|
||||
: t("shared_info.shared_locally", { link })} />
|
||||
)}
|
||||
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
|
||||
</InfoBar>
|
||||
);
|
||||
}
|
||||
|
||||
export function useShareInfo(note: FNote | null | undefined) {
|
||||
const [ link, setLink ] = useState<string>();
|
||||
const [ linkHref, setLinkHref ] = useState<string>();
|
||||
const [ syncServerHost ] = useTriliumOption("syncServerHost");
|
||||
|
||||
function refresh() {
|
||||
if (!note) return;
|
||||
@@ -37,9 +53,10 @@ export default function SharedInfo() {
|
||||
}
|
||||
|
||||
setLink(`<a href="${link}" class="external tn-link">${link}</a>`);
|
||||
setLinkHref(link);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
useEffect(refresh, [ note, syncServerHost ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.name?.startsWith("_share") && attributes.isAffecting(attr, note))) {
|
||||
refresh();
|
||||
@@ -48,16 +65,7 @@ export default function SharedInfo() {
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<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" }} />
|
||||
</InfoBar>
|
||||
)
|
||||
return { link, linkHref, isSharedExternally: !!syncServerHost };
|
||||
}
|
||||
|
||||
function getShareId(note: FNote) {
|
||||
@@ -66,4 +74,4 @@ function getShareId(note: FNote) {
|
||||
}
|
||||
|
||||
return note.getOwnedLabelValue("shareAlias") || note.noteId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,24 +26,13 @@ import ws from "../../services/ws";
|
||||
import appContext from "../../components/app_context";
|
||||
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
|
||||
import options from "../../services/options";
|
||||
import FNote from "../../entities/fnote";
|
||||
|
||||
/**
|
||||
* Displays the full list of attachments of a note and allows the user to interact with them.
|
||||
*/
|
||||
export function AttachmentList({ note }: TypeWidgetProps) {
|
||||
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
|
||||
|
||||
function refresh() {
|
||||
note.getAttachments().then(attachments => setAttachments(Array.from(attachments)));
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
const attachments = useAttachments(note);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,7 +48,25 @@ export function AttachmentList({ note }: TypeWidgetProps) {
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useAttachments(note: FNote) {
|
||||
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
|
||||
|
||||
function refresh() {
|
||||
note.getAttachments().then(attachments => setAttachments(Array.from(attachments)));
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
function AttachmentListHeader({ noteId }: { noteId: string }) {
|
||||
|
||||
@@ -3,12 +3,15 @@ import NoteMapEl from "../note_map/NoteMap";
|
||||
import { useRef } from "preact/hooks";
|
||||
import "./NoteMap.css";
|
||||
|
||||
export default function NoteMap({ note }: TypeWidgetProps) {
|
||||
export default function NoteMap({ note, noteContext }: TypeWidgetProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<NoteMapEl parentRef={containerRef} note={note} widgetMode="type" />
|
||||
<NoteMapEl
|
||||
parentRef={containerRef}
|
||||
note={note}
|
||||
widgetMode={noteContext?.viewScope?.viewMode === "note-map" ? "ribbon" : "type"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import FormText from "../../react/FormText";
|
||||
import OptionsSection from "./components/OptionsSection"
|
||||
import Column from "../../react/Column";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import { experimentalFeatures } from "../../../services/experimental_features";
|
||||
import { useTriliumOptionJson } from "../../react/hooks";
|
||||
|
||||
export default function AdvancedSettings() {
|
||||
return <>
|
||||
@@ -14,6 +17,7 @@ export default function AdvancedSettings() {
|
||||
<DatabaseIntegrityOptions />
|
||||
<DatabaseAnonymizationOptions />
|
||||
<VacuumDatabaseOptions />
|
||||
<ExperimentalOptions />
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -44,14 +48,14 @@ function DatabaseIntegrityOptions() {
|
||||
return (
|
||||
<OptionsSection title={t("database_integrity_check.title")}>
|
||||
<FormText>{t("database_integrity_check.description")}</FormText>
|
||||
|
||||
|
||||
<Button
|
||||
text={t("database_integrity_check.check_button")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("database_integrity_check.checking_integrity"));
|
||||
|
||||
|
||||
const { results } = await server.get<DatabaseCheckIntegrityResponse>("database/check-integrity");
|
||||
|
||||
|
||||
if (results.length === 1 && results[0].integrity_check === "ok") {
|
||||
toast.showMessage(t("database_integrity_check.integrity_check_succeeded"));
|
||||
} else {
|
||||
@@ -93,7 +97,7 @@ function DatabaseAnonymizationOptions() {
|
||||
buttonClick={async () => {
|
||||
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
|
||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
|
||||
|
||||
|
||||
if (!resp.success) {
|
||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
@@ -141,7 +145,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
|
||||
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>
|
||||
}
|
||||
|
||||
return (
|
||||
return (
|
||||
<table className="table table-stripped">
|
||||
<thead>
|
||||
<th>{t("database_anonymization.existing_anonymized_databases")}</th>
|
||||
@@ -154,7 +158,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function VacuumDatabaseOptions() {
|
||||
@@ -171,5 +175,23 @@ function VacuumDatabaseOptions() {
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ExperimentalOptions() {
|
||||
const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson<string[]>("experimentalFeatures", true);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("experimental_features.title")}>
|
||||
<FormText>{t("experimental_features.disclaimer")}</FormText>
|
||||
|
||||
<CheckboxList
|
||||
values={experimentalFeatures}
|
||||
keyProperty="id"
|
||||
titleProperty="name"
|
||||
descriptionProperty="description"
|
||||
currentValue={enabledExperimentalFeatures} onChange={setEnabledExperimentalFeatures}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import FormCheckbox from "../../../react/FormCheckbox";
|
||||
|
||||
interface CheckboxListProps<T> {
|
||||
values: T[];
|
||||
keyProperty: keyof T;
|
||||
titleProperty?: keyof T;
|
||||
disabledProperty?: keyof T;
|
||||
descriptionProperty?: keyof T;
|
||||
currentValue: string[];
|
||||
onChange: (newValues: string[]) => void;
|
||||
columnWidth?: string;
|
||||
}
|
||||
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, descriptionProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
function toggleValue(value: string) {
|
||||
if (currentValue.includes(value)) {
|
||||
// Already there, needs removing.
|
||||
@@ -22,20 +25,17 @@ export default function CheckboxList<T>({ values, keyProperty, titleProperty, di
|
||||
return (
|
||||
<ul style={{ listStyleType: "none", marginBottom: 0, columnWidth: columnWidth ?? "400px" }}>
|
||||
{values.map(value => (
|
||||
<li>
|
||||
<label className="tn-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
value={String(value[keyProperty])}
|
||||
checked={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
onChange={e => toggleValue((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
</label>
|
||||
<li key={String(value[keyProperty])}>
|
||||
<FormCheckbox
|
||||
label={String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
name={String(value[keyProperty])}
|
||||
currentValue={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
hint={value && (descriptionProperty ? String(value[descriptionProperty]) : undefined)}
|
||||
onChange={() => toggleValue(String(value[keyProperty]))}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
import { Locale } from "@triliumnext/commons";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import Dropdown from "../../../react/Dropdown";
|
||||
import { FormDropdownDivider, FormListItem } from "../../../react/FormList";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
|
||||
export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: {
|
||||
id?: string;
|
||||
locales: Locale[],
|
||||
currentValue: string,
|
||||
currentValue: string | null | undefined,
|
||||
onChange: (newLocale: string) => void,
|
||||
defaultLocale?: Locale,
|
||||
extraChildren?: ComponentChildren
|
||||
extraChildren?: ComponentChildren,
|
||||
}) {
|
||||
const [ activeLocale, setActiveLocale ] = useState(defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue));
|
||||
const currentValueWithDefault = currentValue ?? defaultLocale?.id ?? "";
|
||||
const { activeLocale, processedLocales } = useProcessedLocales(locales, defaultLocale, currentValueWithDefault);
|
||||
return (
|
||||
<Dropdown id={id} text={activeLocale?.name}>
|
||||
{processedLocales.map((locale, index) => (
|
||||
(typeof locale === "object") ? (
|
||||
<FormListItem
|
||||
key={locale.id}
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentValue}
|
||||
onClick={() => {
|
||||
onChange(locale.id);
|
||||
}}
|
||||
>{locale.name}</FormListItem>
|
||||
) : (
|
||||
<FormDropdownDivider key={`divider-${index}`} />
|
||||
)
|
||||
))}
|
||||
{extraChildren && (
|
||||
<>
|
||||
<FormDropdownDivider />
|
||||
{extraChildren}
|
||||
</>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function useProcessedLocales(locales: Locale[], defaultLocale: Locale | undefined, currentValue: string) {
|
||||
const activeLocale = defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue);
|
||||
|
||||
const processedLocales = useMemo(() => {
|
||||
const leftToRightLanguages = locales.filter((l) => !l.rtl);
|
||||
@@ -34,29 +64,8 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc
|
||||
];
|
||||
}
|
||||
|
||||
if (extraChildren) {
|
||||
items.push("---");
|
||||
}
|
||||
return items;
|
||||
}, [ locales ]);
|
||||
}, [ locales, defaultLocale ]);
|
||||
|
||||
return (
|
||||
<Dropdown id={id} text={activeLocale?.name}>
|
||||
{processedLocales.map(locale => {
|
||||
if (typeof locale === "object") {
|
||||
return <FormListItem
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentValue}
|
||||
onClick={() => {
|
||||
setActiveLocale(locale);
|
||||
onChange(locale.id);
|
||||
}}
|
||||
>{locale.name}</FormListItem>
|
||||
} else {
|
||||
return <FormDropdownDivider />
|
||||
}
|
||||
})}
|
||||
{extraChildren}
|
||||
</Dropdown>
|
||||
)
|
||||
return { activeLocale, processedLocales };
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "39.2.6",
|
||||
"electron": "39.2.7",
|
||||
"@electron-forge/cli": "7.10.2",
|
||||
"@electron-forge/maker-deb": "7.10.2",
|
||||
"@electron-forge/maker-dmg": "7.10.2",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "39.2.6",
|
||||
"electron": "39.2.7",
|
||||
"fs-extra": "11.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
import type { BrowserContext } from "@playwright/test";
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
interface GotoOpts {
|
||||
url?: string;
|
||||
@@ -123,7 +123,7 @@ export default class App {
|
||||
const noteActionsButton = this.currentNoteSplit.locator(".note-actions");
|
||||
await noteActionsButton.click();
|
||||
|
||||
const dropdownMenu = noteActionsButton.locator(".dropdown-menu");
|
||||
const dropdownMenu = noteActionsButton.locator(".dropdown-menu").first();
|
||||
await this.page.waitForTimeout(100);
|
||||
await expect(dropdownMenu).toBeVisible();
|
||||
dropdownMenu.getByText(itemToFind).click();
|
||||
@@ -163,7 +163,7 @@ export default class App {
|
||||
}
|
||||
|
||||
dropdown(_locator: Locator): DropdownLocator {
|
||||
let locator = _locator as DropdownLocator;
|
||||
const locator = _locator as DropdownLocator;
|
||||
locator.selectOptionByText = async (text: string) => {
|
||||
await locator.locator(".dropdown-toggle").click();
|
||||
await locator.locator(".dropdown-item", { hasText: text }).click();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user