Compare commits
186 Commits
react/prom
...
siriusbcd_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0227449c55 | ||
|
|
f57e90b35c | ||
|
|
5a5d242ea0 | ||
|
|
4afea27fa5 | ||
|
|
fc8042aa25 | ||
|
|
2b6220beb8 | ||
|
|
78426a6c7b | ||
|
|
620e53c255 | ||
|
|
753fc6c769 | ||
|
|
3d6e1dfc0a | ||
|
|
d92431ad65 | ||
|
|
be19d1f5b5 | ||
|
|
ca08a52998 | ||
|
|
e54822f3b0 | ||
|
|
3863e657ef | ||
|
|
341ef79b49 | ||
|
|
335f34b824 | ||
|
|
c864863be4 | ||
|
|
c3ebef0dde | ||
|
|
7b7058c77b | ||
|
|
192cf9bc26 | ||
|
|
1cccbcfabe | ||
|
|
a85b37985a | ||
|
|
8b6b1ee315 | ||
|
|
021c655a1a | ||
|
|
8af8968b49 | ||
|
|
17298edfcc | ||
|
|
5281e8e5b4 | ||
|
|
cc0e30e3f5 | ||
|
|
497bb35209 | ||
|
|
7d1453ffbd | ||
|
|
89228f264f | ||
|
|
a10d99f938 | ||
|
|
d014ae4fcf | ||
|
|
a22687e2d8 | ||
|
|
44475853df | ||
|
|
bbcc670655 | ||
|
|
ae58b4af35 | ||
|
|
fbc2ffac59 | ||
|
|
f279839e6f | ||
|
|
1844a7d666 | ||
|
|
c9f648fcb8 | ||
|
|
948688a4ea | ||
|
|
35ca295d48 | ||
|
|
cacc4ad01d | ||
|
|
b12085f61f | ||
|
|
39dacafa82 | ||
|
|
57deb36027 | ||
|
|
2840df82f4 | ||
|
|
3d971108b8 | ||
|
|
5bcdce72ef | ||
|
|
398329a219 | ||
|
|
754a06343f | ||
|
|
55a79e5fbf | ||
|
|
c78a97fed1 | ||
|
|
13c8ff5cb3 | ||
|
|
9e34d3a668 | ||
|
|
57f220e64c | ||
|
|
a89756a76c | ||
|
|
88c1aa163e | ||
|
|
d2184682e5 | ||
|
|
63cc5b21b4 | ||
|
|
b7703fc4df | ||
|
|
254d3a1c8e | ||
|
|
8d3892757a | ||
|
|
a8992d08b3 | ||
|
|
5e35aa8079 | ||
|
|
df8da0fd4f | ||
|
|
f820c6f23b | ||
|
|
0c616fecdf | ||
|
|
092a84693f | ||
|
|
d1e80815d5 | ||
|
|
0f000ccd93 | ||
|
|
f90e0767cb | ||
|
|
ad6d61f1f7 | ||
|
|
47f7968dc4 | ||
|
|
455b190a5b | ||
|
|
0bc8584c35 | ||
|
|
da39cdb27f | ||
|
|
769c2e9b4e | ||
|
|
783d2b8843 | ||
|
|
baca0a17c3 | ||
|
|
f48d47bac5 | ||
|
|
14fa5d2723 | ||
|
|
70845611a4 | ||
|
|
7be11da85f | ||
|
|
f2f4b0e75b | ||
|
|
491cd27f2d | ||
|
|
7b62881113 | ||
|
|
22f46919f9 | ||
|
|
1ef7fd401f | ||
|
|
5f1dbc23b4 | ||
|
|
8d750417ec | ||
|
|
52f30052d5 | ||
|
|
655e6bafd1 | ||
|
|
d4dfb0cb53 | ||
|
|
8d08973d48 | ||
|
|
9b1b56a381 | ||
|
|
f709c27329 | ||
|
|
02859039ec | ||
|
|
e6810ef753 | ||
|
|
ef86e195c6 | ||
|
|
d69dd2a83f | ||
|
|
5dd21ac539 | ||
|
|
bd6575982b | ||
|
|
80313527c5 | ||
|
|
78901e03d7 | ||
|
|
01c1b19601 | ||
|
|
1d837092a2 | ||
|
|
bde04919fe | ||
|
|
0dd0416346 | ||
|
|
711dd64093 | ||
|
|
db7b4829b5 | ||
|
|
f97c63fe93 | ||
|
|
cb5fe95768 | ||
|
|
34359dd7b6 | ||
|
|
476d1d274e | ||
|
|
c52265c046 | ||
|
|
4fbf3d79c7 | ||
|
|
e8cc92db95 | ||
|
|
40e969bab9 | ||
|
|
3df2105016 | ||
|
|
0aa3cc3d6f | ||
|
|
666f26f516 | ||
|
|
7662dde294 | ||
|
|
d28dda876c | ||
|
|
1ca46e3505 | ||
|
|
6b6dc47f2b | ||
|
|
98b5b81d7d | ||
|
|
48a20500f8 | ||
|
|
a3bd15e102 | ||
|
|
f728b2b0e7 | ||
|
|
50dfd1d329 | ||
|
|
c0375b34fd | ||
|
|
d8e7832f07 | ||
|
|
f2415916aa | ||
|
|
7f67b2a1ee | ||
|
|
bc9ad5012e | ||
|
|
096deda23c | ||
|
|
8b5544268b | ||
|
|
d492c0e091 | ||
|
|
20b301ac0e | ||
|
|
bacbe9f47c | ||
|
|
4ecb693be5 | ||
|
|
454310c3e4 | ||
|
|
e51daad5da | ||
|
|
b13c0fe7a2 | ||
|
|
3036d18df5 | ||
|
|
5dbe9e7da6 | ||
|
|
fd9b6e9e67 | ||
|
|
4f580a37a3 | ||
|
|
f64d11b7c8 | ||
|
|
2bf9e0edd9 | ||
|
|
b807079c55 | ||
|
|
2cc5b896e0 | ||
|
|
7c79caba78 | ||
|
|
fbf4a910fa | ||
|
|
95947a9f8c | ||
|
|
1c05acf5ed | ||
|
|
5cc5f3ffae | ||
|
|
54556c73e2 | ||
|
|
5aa63ac50c | ||
|
|
38eaa94a53 | ||
|
|
7810f6c8da | ||
|
|
6d94efb6c8 | ||
|
|
e89646ee7c | ||
|
|
dee8c115ab | ||
|
|
54d3936c7b | ||
|
|
02452a0513 | ||
|
|
e9f40c48e3 | ||
|
|
6b74b227cb | ||
|
|
00874840b7 | ||
|
|
d79a23bc9e | ||
|
|
3015576d7e | ||
|
|
46c2e162f0 | ||
|
|
3c42577da4 | ||
|
|
76f791da93 | ||
|
|
0c5adcee2d | ||
|
|
b11b3ff67f | ||
|
|
e006afc5a2 | ||
|
|
40dbb818c5 | ||
|
|
62dc570d38 | ||
|
|
b759c5e7d2 | ||
|
|
309d7e704c | ||
|
|
ecf9ce586c | ||
|
|
d0de9e5e21 |
4
.github/workflows/main-docker.yml
vendored
@@ -155,6 +155,10 @@ jobs:
|
|||||||
- name: Update build info
|
- name: Update build info
|
||||||
run: pnpm run chore:update-build-info
|
run: pnpm run chore:update-build-info
|
||||||
|
|
||||||
|
- name: Update nightly version
|
||||||
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: pnpm run chore:ci-update-nightly-version
|
||||||
|
|
||||||
- name: Run the TypeScript build
|
- name: Run the TypeScript build
|
||||||
run: pnpm run server:build
|
run: pnpm run server:build
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/nightly.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Update nightly version
|
- name: Update nightly version
|
||||||
run: npm run chore:ci-update-nightly-version
|
run: pnpm run chore:ci-update-nightly-version
|
||||||
- name: Run the build
|
- name: Run the build
|
||||||
uses: ./.github/actions/build-electron
|
uses: ./.github/actions/build-electron
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -38,15 +38,15 @@
|
|||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
"@stylistic/eslint-plugin": "5.5.0",
|
"@stylistic/eslint-plugin": "5.5.0",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.5",
|
||||||
"@types/node": "24.10.0",
|
"@types/node": "24.10.1",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.35",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"jsdoc": "4.0.5",
|
"jsdoc": "4.0.5",
|
||||||
"lorem-ipsum": "2.0.8",
|
"lorem-ipsum": "2.0.8",
|
||||||
"rcedit": "5.0.0",
|
"rcedit": "5.0.1",
|
||||||
"rimraf": "6.1.0",
|
"rimraf": "6.1.0",
|
||||||
"tslib": "2.8.1"
|
"tslib": "2.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Elian Doran <contact@eliandoran.me>",
|
"author": "Elian Doran <contact@eliandoran.me>",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"packageManager": "pnpm@10.21.0",
|
"packageManager": "pnpm@10.22.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/cli": "2.11.1",
|
"@redocly/cli": "2.11.1",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@fullcalendar/timegrid": "6.1.19",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||||
"@mermaid-js/layout-elk": "0.2.0",
|
"@mermaid-js/layout-elk": "0.2.0",
|
||||||
"@mind-elixir/node-menu": "5.0.0",
|
"@mind-elixir/node-menu": "5.0.1",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@triliumnext/ckeditor5": "workspace:*",
|
"@triliumnext/ckeditor5": "workspace:*",
|
||||||
"@triliumnext/codemirror": "workspace:*",
|
"@triliumnext/codemirror": "workspace:*",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"autocomplete.js": "0.38.1",
|
"autocomplete.js": "0.38.1",
|
||||||
"bootstrap": "5.3.8",
|
"bootstrap": "5.3.8",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"color": "5.0.2",
|
"color": "5.0.3",
|
||||||
"dayjs": "1.11.19",
|
"dayjs": "1.11.19",
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
"debounce": "3.0.0",
|
"debounce": "3.0.0",
|
||||||
@@ -55,11 +55,11 @@
|
|||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "16.4.2",
|
"marked": "16.4.2",
|
||||||
"mermaid": "11.12.1",
|
"mermaid": "11.12.1",
|
||||||
"mind-elixir": "5.3.5",
|
"mind-elixir": "5.3.6",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"react-i18next": "16.2.4",
|
"react-i18next": "16.3.3",
|
||||||
"reveal.js": "5.2.1",
|
"reveal.js": "5.2.1",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.3.1",
|
"tabulator-tables": "6.3.1",
|
||||||
|
|||||||
@@ -647,7 +647,32 @@ export default class TabManager extends Component {
|
|||||||
...this.noteContexts.slice(-noteContexts.length),
|
...this.noteContexts.slice(-noteContexts.length),
|
||||||
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
|
||||||
];
|
];
|
||||||
this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null) });
|
|
||||||
|
// Update mainNtxId if the restored pane is the main pane in the split pane
|
||||||
|
const { oldMainNtxId, newMainNtxId } = (() => {
|
||||||
|
if (noteContexts.length !== 1) {
|
||||||
|
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainNtxId = noteContexts[0]?.mainNtxId;
|
||||||
|
const index = this.noteContexts.findIndex(c => c.ntxId === mainNtxId);
|
||||||
|
|
||||||
|
// No need to update if the restored position is after mainNtxId
|
||||||
|
if (index === -1 || lastClosedTab.position > index) {
|
||||||
|
return { oldMainNtxId: undefined, newMainNtxId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
oldMainNtxId: this.noteContexts[index].ntxId ?? undefined,
|
||||||
|
newMainNtxId: noteContexts[0]?.ntxId ?? undefined
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.triggerCommand("noteContextReorder", {
|
||||||
|
ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null),
|
||||||
|
oldMainNtxId,
|
||||||
|
newMainNtxId
|
||||||
|
});
|
||||||
|
|
||||||
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
|
||||||
if (mainNtx) {
|
if (mainNtx) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { Froca } from "../services/froca-interface.js";
|
|||||||
import type FAttachment from "./fattachment.js";
|
import type FAttachment from "./fattachment.js";
|
||||||
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
import search from "../services/search.js";
|
||||||
|
|
||||||
const LABEL = "label";
|
const LABEL = "label";
|
||||||
const RELATION = "relation";
|
const RELATION = "relation";
|
||||||
@@ -255,6 +256,23 @@ export default class FNote {
|
|||||||
return this.children;
|
return this.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChildNoteIdsWithArchiveFiltering(includeArchived = false) {
|
||||||
|
const isHiddenNote = this.noteId.startsWith("_");
|
||||||
|
const isSearchNote = this.type === "search";
|
||||||
|
if (!includeArchived && !isHiddenNote && !isSearchNote) {
|
||||||
|
const unorderedIds = new Set(await search.searchForNoteIds(`note.parents.noteId="${this.noteId}" #!archived`));
|
||||||
|
const results: string[] = [];
|
||||||
|
for (const id of this.children) {
|
||||||
|
if (unorderedIds.has(id)) {
|
||||||
|
results.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
return this.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getSubtreeNoteIds(includeArchived = false) {
|
async getSubtreeNoteIds(includeArchived = false) {
|
||||||
let noteIds: (string | string[])[] = [];
|
let noteIds: (string | string[])[] = [];
|
||||||
for (const child of await this.getChildNotes()) {
|
for (const child of await this.getChildNotes()) {
|
||||||
@@ -788,6 +806,16 @@ export default class FNote {
|
|||||||
return this.getAttributeValue(LABEL, name);
|
return this.getAttributeValue(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLabelOrRelation(nameWithPrefix: string) {
|
||||||
|
if (nameWithPrefix.startsWith("#")) {
|
||||||
|
return this.getLabelValue(nameWithPrefix.substring(1));
|
||||||
|
} else if (nameWithPrefix.startsWith("~")) {
|
||||||
|
return this.getRelationValue(nameWithPrefix.substring(1));
|
||||||
|
} else {
|
||||||
|
return this.getLabelValue(nameWithPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @returns relation value if relation exists, null otherwise
|
* @returns relation value if relation exists, null otherwise
|
||||||
@@ -839,8 +867,7 @@ export default class FNote {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const promotedAttrs = this.getAttributes()
|
const promotedAttrs = this.getAttributeDefinitions()
|
||||||
.filter((attr) => attr.isDefinition())
|
|
||||||
.filter((attr) => {
|
.filter((attr) => {
|
||||||
const def = attr.getDefinition();
|
const def = attr.getDefinition();
|
||||||
|
|
||||||
@@ -860,6 +887,11 @@ export default class FNote {
|
|||||||
return promotedAttrs;
|
return promotedAttrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAttributeDefinitions() {
|
||||||
|
return this.getAttributes()
|
||||||
|
.filter((attr) => attr.isDefinition());
|
||||||
|
}
|
||||||
|
|
||||||
hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) {
|
hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) {
|
||||||
if (this.noteId === ancestorNoteId) {
|
if (this.noteId === ancestorNoteId) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.
|
|||||||
import ApiLog from "../widgets/api_log.jsx";
|
import ApiLog from "../widgets/api_log.jsx";
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||||
import ContentHeader from "../widgets/containers/content-header.js";
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
import FindWidget from "../widgets/find.js";
|
import FindWidget from "../widgets/find.js";
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import LauncherContainer from "../widgets/containers/launcher_container.js";
|
|||||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import NoteTitleWidget from "../widgets/note_title.js";
|
||||||
import ContentHeader from "../widgets/containers/content-header.js";
|
import ContentHeader from "../widgets/containers/content_header.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ const HIDDEN_ATTRIBUTES = [
|
|||||||
"viewType",
|
"viewType",
|
||||||
"geolocation",
|
"geolocation",
|
||||||
"docName",
|
"docName",
|
||||||
"webViewSrc"
|
"webViewSrc",
|
||||||
|
"archived"
|
||||||
];
|
];
|
||||||
|
|
||||||
async function renderNormalAttributes(note: FNote) {
|
async function renderNormalAttributes(note: FNote) {
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ export async function setLabel(noteId: string, name: string, value: string = "",
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||||
|
await server.put(`notes/${noteId}/set-attribute`, {
|
||||||
|
type: "relation",
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
isInheritable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function removeAttributeById(noteId: string, attributeId: string) {
|
async function removeAttributeById(noteId: string, attributeId: string) {
|
||||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||||
}
|
}
|
||||||
@@ -51,6 +60,23 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a relation identified by its name from the given note, if it exists. Note that the relation must be owned, i.e.
|
||||||
|
* it will not remove inherited attributes.
|
||||||
|
*
|
||||||
|
* @param note the note from which to remove the relation.
|
||||||
|
* @param relationName the name of the relation to remove.
|
||||||
|
* @returns `true` if an attribute was identified and removed, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function removeOwnedRelationByName(note: FNote, relationName: string) {
|
||||||
|
const relation = note.getOwnedRelation(relationName);
|
||||||
|
if (relation) {
|
||||||
|
removeAttributeById(note.noteId, relation.attributeId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
||||||
* For an attribute with an empty value, pass an empty string instead.
|
* For an attribute with an empty value, pass an empty string instead.
|
||||||
@@ -116,8 +142,10 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
|
|||||||
export default {
|
export default {
|
||||||
addLabel,
|
addLabel,
|
||||||
setLabel,
|
setLabel,
|
||||||
|
setRelation,
|
||||||
setAttribute,
|
setAttribute,
|
||||||
removeAttributeById,
|
removeAttributeById,
|
||||||
removeOwnedLabelByName,
|
removeOwnedLabelByName,
|
||||||
|
removeOwnedRelationByName,
|
||||||
isAffecting
|
isAffecting
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ function getHue(color: ColorInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReadableTextColor(bgColor: string) {
|
||||||
|
const colorInstance = Color(bgColor);
|
||||||
|
return colorInstance.isLight() ? "#000" : "#fff";
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createClassForColor
|
createClassForColor
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
|
import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_helper.js";
|
||||||
import { getCurrentLanguage } from "./i18n.js";
|
import { getCurrentLanguage } from "./i18n.js";
|
||||||
import { formatCodeBlocks } from "./syntax_highlight.js";
|
import { formatCodeBlocks } from "./syntax_highlight.js";
|
||||||
|
|
||||||
@@ -10,18 +11,18 @@ export default function renderDoc(note: FNote) {
|
|||||||
if (docName) {
|
if (docName) {
|
||||||
// find doc based on language
|
// find doc based on language
|
||||||
const url = getUrl(docName, getCurrentLanguage());
|
const url = getUrl(docName, getCurrentLanguage());
|
||||||
$content.load(url, (response, status) => {
|
$content.load(url, async (response, status) => {
|
||||||
// fallback to english doc if no translation available
|
// fallback to english doc if no translation available
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
const fallbackUrl = getUrl(docName, "en");
|
const fallbackUrl = getUrl(docName, "en");
|
||||||
$content.load(fallbackUrl, () => {
|
$content.load(fallbackUrl, async () => {
|
||||||
processContent(fallbackUrl, $content)
|
await processContent(fallbackUrl, $content)
|
||||||
resolve($content);
|
resolve($content);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
processContent(url, $content);
|
await processContent(url, $content);
|
||||||
resolve($content);
|
resolve($content);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -32,7 +33,7 @@ export default function renderDoc(note: FNote) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function processContent(url: string, $content: JQuery<HTMLElement>) {
|
async function processContent(url: string, $content: JQuery<HTMLElement>) {
|
||||||
const dir = url.substring(0, url.lastIndexOf("/"));
|
const dir = url.substring(0, url.lastIndexOf("/"));
|
||||||
|
|
||||||
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
|
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
|
||||||
@@ -42,6 +43,9 @@ function processContent(url: string, $content: JQuery<HTMLElement>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
formatCodeBlocks($content);
|
formatCodeBlocks($content);
|
||||||
|
|
||||||
|
// Apply reference links.
|
||||||
|
await applyReferenceLinks($content[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrl(docNameValue: string, language: string) {
|
function getUrl(docNameValue: string, language: string) {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ async function getActionsForScope(scope: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
|
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
|
||||||
|
if (!$el[0]) return [];
|
||||||
|
|
||||||
const actions = await getActionsForScope(scope);
|
const actions = await getActionsForScope(scope);
|
||||||
const bindings: ShortcutBinding[] = [];
|
const bindings: ShortcutBinding[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -150,11 +150,16 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
|
|||||||
$container.append($noteLink);
|
$container.append($noteLink);
|
||||||
|
|
||||||
if (showNotePath) {
|
if (showNotePath) {
|
||||||
|
let pathSegments: string[];
|
||||||
|
if (notePath == "root") {
|
||||||
|
pathSegments = ["⌂"];
|
||||||
|
} else {
|
||||||
const resolvedPathSegments = (await treeService.resolveNotePathToSegments(notePath)) || [];
|
const resolvedPathSegments = (await treeService.resolveNotePathToSegments(notePath)) || [];
|
||||||
resolvedPathSegments.pop(); // Remove last element
|
resolvedPathSegments.pop(); // Remove last element
|
||||||
|
|
||||||
const resolvedPath = resolvedPathSegments.join("/");
|
const resolvedPath = resolvedPathSegments.join("/");
|
||||||
const pathSegments = await treeService.getNotePathTitleComponents(resolvedPath);
|
pathSegments = await treeService.getNotePathTitleComponents(resolvedPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (pathSegments) {
|
if (pathSegments) {
|
||||||
if (pathSegments.length) {
|
if (pathSegments.length) {
|
||||||
@@ -302,7 +307,8 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
|
|||||||
// Right click is handled separately.
|
// Right click is handled separately.
|
||||||
const isMiddleClick = evt && "which" in evt && evt.which === 2;
|
const isMiddleClick = evt && "which" in evt && evt.which === 2;
|
||||||
const targetIsBlank = ($link?.attr("target") === "_blank");
|
const targetIsBlank = ($link?.attr("target") === "_blank");
|
||||||
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
|
const isDoubleClick = isLeftClick && evt?.type === "dblclick";
|
||||||
|
const openInNewTab = (isLeftClick && ctrlKey) || isDoubleClick || isMiddleClick || targetIsBlank;
|
||||||
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
|
||||||
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
|
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
|
||||||
|
|
||||||
@@ -323,20 +329,22 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
|
|||||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||||
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
|
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
|
||||||
|
|
||||||
if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
|
if (openInNewTab || openInNewWindow || (isLeftClick && (withinEditLink || outsideOfCKEditor))) {
|
||||||
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
|
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
|
||||||
window.open(hrefLink, "_blank");
|
window.open(hrefLink, "_blank");
|
||||||
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
|
|
||||||
const electron = utils.dynamicRequire("electron");
|
|
||||||
electron.shell.openPath(hrefLink);
|
|
||||||
} else {
|
} else {
|
||||||
// Enable protocols supported by CKEditor 5 to be clickable.
|
// Enable protocols supported by CKEditor 5 to be clickable.
|
||||||
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
|
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
|
||||||
|
if ( utils.isElectron()) {
|
||||||
|
const electron = utils.dynamicRequire("electron");
|
||||||
|
electron.shell.openExternal(hrefLink);
|
||||||
|
} else {
|
||||||
window.open(hrefLink, "_blank");
|
window.open(hrefLink, "_blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -468,18 +476,9 @@ $(document).on("auxclick", "a", goToLink); // to handle the middle button
|
|||||||
// TODO: Check why the event is not supported.
|
// TODO: Check why the event is not supported.
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
$(document).on("contextmenu", "a", linkContextMenu);
|
$(document).on("contextmenu", "a", linkContextMenu);
|
||||||
$(document).on("dblclick", "a", (e) => {
|
// TODO: Check why the event is not supported.
|
||||||
e.preventDefault();
|
//@ts-ignore
|
||||||
e.stopPropagation();
|
$(document).on("dblclick", "a", goToLink);
|
||||||
|
|
||||||
const $link = $(e.target).closest("a");
|
|
||||||
|
|
||||||
const address = $link.attr("href");
|
|
||||||
|
|
||||||
if (address && address.startsWith("http")) {
|
|
||||||
window.open(address, "_blank");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on("mousedown", "a", (e) => {
|
$(document).on("mousedown", "a", (e) => {
|
||||||
if (e.which === 2) {
|
if (e.which === 2) {
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import type { AttachmentRow, EtapiTokenRow, OptionNames } from "@triliumnext/commons";
|
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
|
||||||
import type { AttributeType } from "../entities/fattribute.js";
|
import type { AttributeType } from "../entities/fattribute.js";
|
||||||
import type { EntityChange } from "../server_types.js";
|
import type { EntityChange } from "../server_types.js";
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
// TODO: Deduplicate with server.
|
||||||
|
|
||||||
interface NoteRow {
|
interface NoteRow {
|
||||||
|
blobId: string;
|
||||||
|
dateCreated: string;
|
||||||
|
dateModified: string;
|
||||||
isDeleted?: boolean;
|
isDeleted?: boolean;
|
||||||
|
isProtected?: boolean;
|
||||||
|
mime: string;
|
||||||
|
noteId: string;
|
||||||
|
title: string;
|
||||||
|
type: NoteType;
|
||||||
|
utcDateCreated: string;
|
||||||
|
utcDateModified: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Deduplicate with BranchRow from `rows.ts`/
|
// TODO: Deduplicate with BranchRow from `rows.ts`/
|
||||||
|
|||||||
@@ -77,11 +77,11 @@ function closePersistent(id: string) {
|
|||||||
$(`#toast-${id}`).remove();
|
$(`#toast-${id}`).remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMessage(message: string, delay = 2000) {
|
function showMessage(message: string, delay = 2000, icon = "check") {
|
||||||
console.debug(utils.now(), "message:", message);
|
console.debug(utils.now(), "message:", message);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
icon: "check",
|
icon,
|
||||||
message: message,
|
message: message,
|
||||||
autohide: true,
|
autohide: true,
|
||||||
delay
|
delay
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
background-color: var(--root-background);
|
background-color: var(--root-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.mobile #root-widget {
|
||||||
|
background-color: var(--main-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--native-titlebar-darwin-x-offset: 10;
|
--native-titlebar-darwin-x-offset: 10;
|
||||||
--native-titlebar-darwin-y-offset: 12 !important;
|
--native-titlebar-darwin-y-offset: 12 !important;
|
||||||
|
|||||||
@@ -690,7 +690,8 @@
|
|||||||
"convert_into_attachment_failed": "笔记 '{{title}}' 转换失败。",
|
"convert_into_attachment_failed": "笔记 '{{title}}' 转换失败。",
|
||||||
"convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
|
"convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
|
||||||
"convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?",
|
"convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?",
|
||||||
"print_pdf": "导出为 PDF..."
|
"print_pdf": "导出为 PDF...",
|
||||||
|
"open_note_on_server": "在服务器上打开笔记"
|
||||||
},
|
},
|
||||||
"onclick_button": {
|
"onclick_button": {
|
||||||
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
|
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
|
||||||
@@ -1110,7 +1111,8 @@
|
|||||||
"title": "内容宽度",
|
"title": "内容宽度",
|
||||||
"default_description": "Trilium默认会限制内容的最大宽度以提高在宽屏中全屏时的可读性。",
|
"default_description": "Trilium默认会限制内容的最大宽度以提高在宽屏中全屏时的可读性。",
|
||||||
"max_width_label": "内容最大宽度(像素)",
|
"max_width_label": "内容最大宽度(像素)",
|
||||||
"max_width_unit": "像素"
|
"max_width_unit": "像素",
|
||||||
|
"centerContent": "保持内容居中"
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "原生标题栏(需要重新启动应用)",
|
"title": "原生标题栏(需要重新启动应用)",
|
||||||
@@ -2082,5 +2084,11 @@
|
|||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
"delete_note": "删除笔记..."
|
"delete_note": "删除笔记..."
|
||||||
|
},
|
||||||
|
"read-only-info": {
|
||||||
|
"read-only-note": "当前正在查看一个只读笔记。",
|
||||||
|
"auto-read-only-note": "这条笔记以只读模式显示便于快速加载。",
|
||||||
|
"auto-read-only-learn-more": "了解更多",
|
||||||
|
"edit-note": "编辑笔记"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2035,7 +2035,8 @@
|
|||||||
"add-column": "Add Column",
|
"add-column": "Add Column",
|
||||||
"add-column-placeholder": "Enter column name...",
|
"add-column-placeholder": "Enter column name...",
|
||||||
"edit-note-title": "Click to edit note title",
|
"edit-note-title": "Click to edit note title",
|
||||||
"edit-column-title": "Click to edit column title"
|
"edit-column-title": "Click to edit column title",
|
||||||
|
"column-already-exists": "This column already exists on the board."
|
||||||
},
|
},
|
||||||
"presentation_view": {
|
"presentation_view": {
|
||||||
"edit-slide": "Edit this slide",
|
"edit-slide": "Edit this slide",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"prefix": "接頭辞: ",
|
"prefix": "接頭辞: ",
|
||||||
"branch_prefix_saved": "ブランチの接頭辞が保存されました。",
|
"branch_prefix_saved": "ブランチの接頭辞が保存されました。",
|
||||||
"edit_branch_prefix_multiple": "{{count}} ブランチのブランチ接頭辞を編集",
|
"edit_branch_prefix_multiple": "{{count}} ブランチのブランチ接頭辞を編集",
|
||||||
"branch_prefix_saved_multiple": "{{count}} 個のブランチのブランチ接頭辞が保存されました。",
|
"branch_prefix_saved_multiple": "{{count}} ブランチのブランチ接頭辞が保存されました。",
|
||||||
"affected_branches": "影響を受けるブランチ {{count}}:"
|
"affected_branches": "影響を受けるブランチ {{count}}:"
|
||||||
},
|
},
|
||||||
"global_menu": {
|
"global_menu": {
|
||||||
@@ -456,7 +456,8 @@
|
|||||||
"convert_into_attachment_failed": "ノート '{{title}}' の変換に失敗しました。",
|
"convert_into_attachment_failed": "ノート '{{title}}' の変換に失敗しました。",
|
||||||
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
|
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
|
||||||
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
|
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
|
||||||
"note_attachments": "ノートの添付ファイル"
|
"note_attachments": "ノートの添付ファイル",
|
||||||
|
"open_note_on_server": "サーバー上のノートを開く"
|
||||||
},
|
},
|
||||||
"command_palette": {
|
"command_palette": {
|
||||||
"export_note_title": "ノートをエクスポート",
|
"export_note_title": "ノートをエクスポート",
|
||||||
|
|||||||
@@ -302,7 +302,10 @@
|
|||||||
"edit_branch_prefix": "Editează prefixul ramurii",
|
"edit_branch_prefix": "Editează prefixul ramurii",
|
||||||
"help_on_tree_prefix": "Informații despre prefixe de ierarhie",
|
"help_on_tree_prefix": "Informații despre prefixe de ierarhie",
|
||||||
"prefix": "Prefix: ",
|
"prefix": "Prefix: ",
|
||||||
"save": "Salvează"
|
"save": "Salvează",
|
||||||
|
"edit_branch_prefix_multiple": "Editează prefixul pentru {{count}} ramuri",
|
||||||
|
"branch_prefix_saved_multiple": "Prefixul a fost modificat pentru {{count}} ramuri.",
|
||||||
|
"affected_branches": "Ramuri afectate ({{count}}):"
|
||||||
},
|
},
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
"affected_notes": "Notițe afectate",
|
"affected_notes": "Notițe afectate",
|
||||||
@@ -537,7 +540,8 @@
|
|||||||
"opml_version_1": "OPML v1.0 - text simplu",
|
"opml_version_1": "OPML v1.0 - text simplu",
|
||||||
"opml_version_2": "OPML v2.0 - permite și HTML",
|
"opml_version_2": "OPML v2.0 - permite și HTML",
|
||||||
"format_html": "HTML - recomandat deoarece păstrează toata formatarea",
|
"format_html": "HTML - recomandat deoarece păstrează toata formatarea",
|
||||||
"format_pdf": "PDF - cu scopul de printare sau partajare."
|
"format_pdf": "PDF - cu scopul de printare sau partajare.",
|
||||||
|
"share-format": "HTML pentru publicare web - folosește aceeași temă pentru notițele partajate, dar se pot publica într-un website static."
|
||||||
},
|
},
|
||||||
"fast_search": {
|
"fast_search": {
|
||||||
"description": "Căutarea rapidă dezactivează căutarea la nivel de conținut al notițelor cu scopul de a îmbunătăți performanța de căutare pentru baze de date mari.",
|
"description": "Căutarea rapidă dezactivează căutarea la nivel de conținut al notițelor cu scopul de a îmbunătăți performanța de căutare pentru baze de date mari.",
|
||||||
@@ -753,7 +757,8 @@
|
|||||||
"placeholder": "Introduceți etichetele HTML, câte unul pe linie",
|
"placeholder": "Introduceți etichetele HTML, câte unul pe linie",
|
||||||
"reset_button": "Resetează la lista implicită",
|
"reset_button": "Resetează la lista implicită",
|
||||||
"title": "Etichete HTML la importare"
|
"title": "Etichete HTML la importare"
|
||||||
}
|
},
|
||||||
|
"importZipRecommendation": "Când importați un fișier ZIP, ierarhia notițelor va reflecta structura subdirectoarelor din arhivă."
|
||||||
},
|
},
|
||||||
"include_archived_notes": {
|
"include_archived_notes": {
|
||||||
"include_archived_notes": "Include notițele arhivate"
|
"include_archived_notes": "Include notițele arhivate"
|
||||||
@@ -799,7 +804,8 @@
|
|||||||
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
|
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
|
||||||
"max_width_label": "Lungimea maximă a conținutului",
|
"max_width_label": "Lungimea maximă a conținutului",
|
||||||
"max_width_unit": "pixeli",
|
"max_width_unit": "pixeli",
|
||||||
"title": "Lățime conținut"
|
"title": "Lățime conținut",
|
||||||
|
"centerContent": "Centrează conținutul"
|
||||||
},
|
},
|
||||||
"mobile_detail_menu": {
|
"mobile_detail_menu": {
|
||||||
"delete_this_note": "Șterge această notiță",
|
"delete_this_note": "Șterge această notiță",
|
||||||
@@ -856,7 +862,8 @@
|
|||||||
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
|
"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_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?",
|
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?",
|
||||||
"print_pdf": "Exportare ca PDF..."
|
"print_pdf": "Exportare ca PDF...",
|
||||||
|
"open_note_on_server": "Deschide notița pe server"
|
||||||
},
|
},
|
||||||
"note_erasure_timeout": {
|
"note_erasure_timeout": {
|
||||||
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
|
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
|
||||||
@@ -1246,11 +1253,11 @@
|
|||||||
"timeout_unit": "milisecunde"
|
"timeout_unit": "milisecunde"
|
||||||
},
|
},
|
||||||
"table_of_contents": {
|
"table_of_contents": {
|
||||||
"description": "Tabela de conținut va apărea în notițele de tip text atunci când notița are un număr de titluri mai mare decât cel definit. Acest număr se poate personaliza:",
|
"description": "Cuprinsul va apărea în notițele de tip text atunci când notița are un număr de titluri mai mare decât cel definit. Acest număr se poate personaliza:",
|
||||||
"unit": "titluri",
|
"unit": "titluri",
|
||||||
"disable_info": "De asemenea se poate dezactiva tabela de conținut setând o valoare foarte mare.",
|
"disable_info": "De asemenea se poate dezactiva cuprinsul setând o valoare foarte mare.",
|
||||||
"shortcut_info": "Se poate configura și o scurtatură pentru a comuta rapid vizibilitatea panoului din dreapta (inclusiv tabela de conținut) în Opțiuni -> Scurtături (denumirea „toggleRightPane”).",
|
"shortcut_info": "Se poate configura și o scurtatură pentru a comuta rapid vizibilitatea panoului din dreapta (inclusiv cuprinsul) în Opțiuni -> Scurtături (denumirea „toggleRightPane”).",
|
||||||
"title": "Tabelă de conținut"
|
"title": "Cuprins"
|
||||||
},
|
},
|
||||||
"text_auto_read_only_size": {
|
"text_auto_read_only_size": {
|
||||||
"description": "Marchează pragul în care o notiță de o anumită dimensiune va fi afișată în mod de citire (pentru motive de performanță).",
|
"description": "Marchează pragul în care o notiță de o anumită dimensiune va fi afișată în mod de citire (pentru motive de performanță).",
|
||||||
@@ -1503,7 +1510,9 @@
|
|||||||
"window-on-top": "Menține fereastra mereu vizibilă"
|
"window-on-top": "Menține fereastra mereu vizibilă"
|
||||||
},
|
},
|
||||||
"note_detail": {
|
"note_detail": {
|
||||||
"could_not_find_typewidget": "Nu s-a putut găsi widget-ul corespunzător tipului „{{type}}”"
|
"could_not_find_typewidget": "Nu s-a putut găsi widget-ul corespunzător tipului „{{type}}”",
|
||||||
|
"printing": "Imprimare în curs...",
|
||||||
|
"printing_pdf": "Exportare ca PDF în curs..."
|
||||||
},
|
},
|
||||||
"note_title": {
|
"note_title": {
|
||||||
"placeholder": "introduceți titlul notiței aici..."
|
"placeholder": "introduceți titlul notiței aici..."
|
||||||
@@ -2014,7 +2023,8 @@
|
|||||||
"new-item-placeholder": "Introduceți titlul notiței...",
|
"new-item-placeholder": "Introduceți titlul notiței...",
|
||||||
"add-column-placeholder": "Introduceți denumirea coloanei...",
|
"add-column-placeholder": "Introduceți denumirea coloanei...",
|
||||||
"edit-note-title": "Clic pentru a edita titlul notiței",
|
"edit-note-title": "Clic pentru a edita titlul notiței",
|
||||||
"edit-column-title": "Clic pentru a edita titlul coloanei"
|
"edit-column-title": "Clic pentru a edita titlul coloanei",
|
||||||
|
"column-already-exists": "Această coloană deja există."
|
||||||
},
|
},
|
||||||
"command_palette": {
|
"command_palette": {
|
||||||
"tree-action-name": "Listă de notițe: {{name}}",
|
"tree-action-name": "Listă de notițe: {{name}}",
|
||||||
@@ -2076,5 +2086,14 @@
|
|||||||
"edit-slide": "Editați acest slide",
|
"edit-slide": "Editați acest slide",
|
||||||
"start-presentation": "Începeți prezentarea",
|
"start-presentation": "Începeți prezentarea",
|
||||||
"slide-overview": "Afișați o imagine de ansamblu a slide-urilor"
|
"slide-overview": "Afișați o imagine de ansamblu a slide-urilor"
|
||||||
|
},
|
||||||
|
"read-only-info": {
|
||||||
|
"read-only-note": "Vizualizați o notiță în modul doar în citire.",
|
||||||
|
"auto-read-only-note": "Această notiță este afișată în modul doar în citire din motive de performanță.",
|
||||||
|
"auto-read-only-learn-more": "Mai multe detalii",
|
||||||
|
"edit-note": "Editează notița"
|
||||||
|
},
|
||||||
|
"calendar_view": {
|
||||||
|
"delete_note": "Șterge notița..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,7 +687,8 @@
|
|||||||
"convert_into_attachment_failed": "筆記 '{{title}}' 轉換失敗。",
|
"convert_into_attachment_failed": "筆記 '{{title}}' 轉換失敗。",
|
||||||
"convert_into_attachment_successful": "筆記 '{{title}}' 已成功轉換為附件。",
|
"convert_into_attachment_successful": "筆記 '{{title}}' 已成功轉換為附件。",
|
||||||
"convert_into_attachment_prompt": "確定要將筆記 '{{title}}' 轉換為父級筆記的附件嗎?",
|
"convert_into_attachment_prompt": "確定要將筆記 '{{title}}' 轉換為父級筆記的附件嗎?",
|
||||||
"print_pdf": "匯出為 PDF…"
|
"print_pdf": "匯出為 PDF…",
|
||||||
|
"open_note_on_server": "在伺服器上開啟筆記"
|
||||||
},
|
},
|
||||||
"onclick_button": {
|
"onclick_button": {
|
||||||
"no_click_handler": "按鈕元件'{{componentId}}'沒有定義點擊時的處理方式"
|
"no_click_handler": "按鈕元件'{{componentId}}'沒有定義點擊時的處理方式"
|
||||||
@@ -1107,7 +1108,8 @@
|
|||||||
"title": "內容寬度",
|
"title": "內容寬度",
|
||||||
"default_description": "Trilium 預設會限制內容的最大寬度以提高在寬螢幕中全螢幕時的可讀性。",
|
"default_description": "Trilium 預設會限制內容的最大寬度以提高在寬螢幕中全螢幕時的可讀性。",
|
||||||
"max_width_label": "內容最大寬度(像素)",
|
"max_width_label": "內容最大寬度(像素)",
|
||||||
"max_width_unit": "像素"
|
"max_width_unit": "像素",
|
||||||
|
"centerContent": "將內容置中"
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "原生標題列(需要重新啟動程式)",
|
"title": "原生標題列(需要重新啟動程式)",
|
||||||
@@ -2082,5 +2084,11 @@
|
|||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
"delete_note": "刪除筆記…"
|
"delete_note": "刪除筆記…"
|
||||||
|
},
|
||||||
|
"read-only-info": {
|
||||||
|
"read-only-note": "目前正在檢視唯讀筆記。",
|
||||||
|
"auto-read-only-note": "此筆記以唯讀模式顯示以加快載入速度。",
|
||||||
|
"auto-read-only-learn-more": "了解更多",
|
||||||
|
"edit-note": "編輯筆記"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.user-attributes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-attributes .user-attribute {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: var(--chip-bg, rgba(0, 0, 0, 0.08));
|
||||||
|
color: var(--chip-fg, inherit);
|
||||||
|
border: 1px solid var(--chip-border, rgba(0, 0, 0, 0.15));
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-attributes .user-attribute:hover {
|
||||||
|
background-color: var(--chip-bg-hover, rgba(0, 0, 0, 0.12));
|
||||||
|
border-color: var(--chip-border-hover, rgba(0, 0, 0, 0.22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-attributes .user-attribute .name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-attributes .user-attribute .value {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
134
apps/client/src/widgets/attribute_widgets/UserAttributesList.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import "./UserAttributesList.css";
|
||||||
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
import attributes from "../../services/attributes";
|
||||||
|
import { DefinitionObject } from "../../services/promoted_attribute_definition_parser";
|
||||||
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
|
import { ComponentChildren, CSSProperties } from "preact";
|
||||||
|
import Icon from "../react/Icon";
|
||||||
|
import NoteLink from "../react/NoteLink";
|
||||||
|
import { getReadableTextColor } from "../../services/css_class_manager";
|
||||||
|
|
||||||
|
interface UserAttributesListProps {
|
||||||
|
note: FNote;
|
||||||
|
ignoredAttributes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttributeWithDefinitions {
|
||||||
|
friendlyName: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
def: DefinitionObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAttributesDisplay({ note, ignoredAttributes }: UserAttributesListProps) {
|
||||||
|
const userAttributes = useNoteAttributesWithDefinitions(note, ignoredAttributes);
|
||||||
|
return userAttributes?.length > 0 && (
|
||||||
|
<div className="user-attributes">
|
||||||
|
{userAttributes?.map(attr => buildUserAttribute(attr))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function useNoteAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {
|
||||||
|
const [ userAttributes, setUserAttributes ] = useState<AttributeWithDefinitions[]>(getAttributesWithDefinitions(note, attributesToIgnore));
|
||||||
|
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||||
|
setUserAttributes(getAttributesWithDefinitions(note, attributesToIgnore));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return userAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserAttribute({ attr, children, style }: { attr: AttributeWithDefinitions, children: ComponentChildren, style?: CSSProperties }) {
|
||||||
|
const className = `${attr.type === "label" ? "label" + " " + attr.def.labelType : "relation"}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={attr.friendlyName} className={`user-attribute type-${className}`} style={style}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
|
||||||
|
const defaultLabel = <><strong>{attr.friendlyName}:</strong>{" "}</>;
|
||||||
|
let content: ComponentChildren;
|
||||||
|
let style: CSSProperties | undefined;
|
||||||
|
|
||||||
|
if (attr.type === "label") {
|
||||||
|
let value = attr.value;
|
||||||
|
switch (attr.def.labelType) {
|
||||||
|
case "number":
|
||||||
|
let formattedValue = value;
|
||||||
|
const numberValue = Number(value);
|
||||||
|
if (!Number.isNaN(numberValue) && attr.def.numberPrecision) formattedValue = numberValue.toFixed(attr.def.numberPrecision);
|
||||||
|
content = <>{defaultLabel}{formattedValue}</>;
|
||||||
|
break;
|
||||||
|
case "date":
|
||||||
|
case "datetime": {
|
||||||
|
const date = new Date(value);
|
||||||
|
const timeFormat = attr.def.labelType !== "date" ? "short" : "none";
|
||||||
|
const formattedValue = formatDateTime(date, "short", timeFormat);
|
||||||
|
content = <>{defaultLabel}{formattedValue}</>;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "time": {
|
||||||
|
const date = new Date(`1970-01-01T${value}Z`);
|
||||||
|
const formattedValue = formatDateTime(date, "none", "short");
|
||||||
|
content = <>{defaultLabel}{formattedValue}</>;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "boolean":
|
||||||
|
content = <><Icon icon={value === "true" ? "bx bx-check-square" : "bx bx-square"} />{" "}<strong>{attr.friendlyName}</strong></>;
|
||||||
|
break;
|
||||||
|
case "url":
|
||||||
|
content = <a href={value} target="_blank" rel="noopener noreferrer">{attr.friendlyName}</a>;
|
||||||
|
break;
|
||||||
|
case "color":
|
||||||
|
style = { backgroundColor: value, color: getReadableTextColor(value) };
|
||||||
|
content = <>{attr.friendlyName}</>;
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
default:
|
||||||
|
content = <>{defaultLabel}{value}</>;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (attr.type === "relation") {
|
||||||
|
content = <>{defaultLabel}<NoteLink notePath={attr.value} showNoteIcon /></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {
|
||||||
|
const attributeDefintions = note.getAttributeDefinitions();
|
||||||
|
const result: AttributeWithDefinitions[] = [];
|
||||||
|
for (const attr of attributeDefintions) {
|
||||||
|
const def = attr.getDefinition();
|
||||||
|
const [ type, name ] = attr.name.split(":", 2);
|
||||||
|
const friendlyName = def?.promotedAlias || name;
|
||||||
|
const props: Omit<AttributeWithDefinitions, "value"> = { def, name, type, friendlyName };
|
||||||
|
|
||||||
|
if (attributesToIgnore.includes(name)) continue;
|
||||||
|
|
||||||
|
if (type === "label") {
|
||||||
|
const labels = note.getLabels(name);
|
||||||
|
for (const label of labels) {
|
||||||
|
if (!label.value) continue;
|
||||||
|
result.push({ ...props, value: label.value } );
|
||||||
|
}
|
||||||
|
} else if (type === "relation") {
|
||||||
|
const relations = note.getRelations(name);
|
||||||
|
for (const relation of relations) {
|
||||||
|
if (!relation.value) continue;
|
||||||
|
result.push({ ...props, value: relation.value } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import ActionButton from "../react/ActionButton";
|
import ActionButton from "../react/ActionButton";
|
||||||
import { useNoteContext, useTriliumEvent } from "../react/hooks";
|
import { useNoteContext, useTriliumEvents } from "../react/hooks";
|
||||||
|
import appContext from "../../components/app_context";
|
||||||
|
|
||||||
export default function ClosePaneButton() {
|
export default function ClosePaneButton() {
|
||||||
const { noteContext, ntxId, parentComponent } = useNoteContext();
|
const { noteContext, ntxId, parentComponent } = useNoteContext();
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
setIsEnabled(!!(noteContext && !!noteContext.mainNtxId));
|
const isMainOfSomeContext = appContext.tabManager.noteContexts.some(c => c.mainNtxId === ntxId);
|
||||||
|
setIsEnabled(!!(noteContext && (!!noteContext.mainNtxId || isMainOfSomeContext)));
|
||||||
}
|
}
|
||||||
|
|
||||||
useTriliumEvent("noteContextReorder", refresh);
|
useTriliumEvents(["noteContextRemoved", "noteContextReorder", "newNoteContextCreated"], refresh);
|
||||||
useEffect(refresh, [ntxId]);
|
useEffect(refresh, [ntxId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ export function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable
|
|||||||
props = {
|
props = {
|
||||||
note, noteIds, notePath,
|
note, noteIds, notePath,
|
||||||
highlightedTokens,
|
highlightedTokens,
|
||||||
viewConfig: viewModeConfig[0],
|
viewConfig: viewModeConfig.config,
|
||||||
saveConfig: viewModeConfig[1],
|
saveConfig: viewModeConfig.storeFn,
|
||||||
onReady: onReady ?? (() => {}),
|
onReady: onReady ?? (() => {}),
|
||||||
...restProps
|
...restProps
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
|
|||||||
|
|
||||||
async function getNoteIds(note: FNote) {
|
async function getNoteIds(note: FNote) {
|
||||||
if (viewType === "list" || viewType === "grid" || viewType === "table" || note.type === "search") {
|
if (viewType === "list" || viewType === "grid" || viewType === "table" || note.type === "search") {
|
||||||
return note.getChildNoteIds();
|
return await note.getChildNoteIdsWithArchiveFiltering(includeArchived);
|
||||||
} else {
|
} else {
|
||||||
return await note.getSubtreeNoteIds(includeArchived);
|
return await note.getSubtreeNoteIds(includeArchived);
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,11 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
|
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
|
||||||
const [ viewConfig, setViewConfig ] = useState<[T | undefined, (data: T) => void]>();
|
const [ viewConfig, setViewConfig ] = useState<{
|
||||||
|
config: T | undefined;
|
||||||
|
storeFn: (data: T) => void;
|
||||||
|
note: FNote;
|
||||||
|
}>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!note || !viewType) return;
|
if (!note || !viewType) return;
|
||||||
@@ -200,12 +204,14 @@ export function useViewModeConfig<T extends object>(note: FNote | null | undefin
|
|||||||
const viewStorage = new ViewModeStorage<T>(note, viewType);
|
const viewStorage = new ViewModeStorage<T>(note, viewType);
|
||||||
viewStorage.restore().then(config => {
|
viewStorage.restore().then(config => {
|
||||||
const storeFn = (config: T) => {
|
const storeFn = (config: T) => {
|
||||||
setViewConfig([ config, storeFn ]);
|
setViewConfig({ note, config, storeFn });
|
||||||
viewStorage.store(config);
|
viewStorage.store(config);
|
||||||
};
|
};
|
||||||
setViewConfig([ config, storeFn ]);
|
setViewConfig({ note, config, storeFn });
|
||||||
});
|
});
|
||||||
}, [ note, viewType ]);
|
}, [ note, viewType ]);
|
||||||
|
|
||||||
|
// Only expose config for the current note, avoid leaking notes when switching between them.
|
||||||
|
if (viewConfig?.note !== note) return undefined;
|
||||||
return viewConfig;
|
return viewConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BulkAction } from "@triliumnext/commons";
|
||||||
import { BoardViewData } from ".";
|
import { BoardViewData } from ".";
|
||||||
import appContext from "../../../components/app_context";
|
import appContext from "../../../components/app_context";
|
||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
@@ -12,15 +13,25 @@ import { ColumnMap } from "./data";
|
|||||||
|
|
||||||
export default class BoardApi {
|
export default class BoardApi {
|
||||||
|
|
||||||
|
private isRelationMode: boolean;
|
||||||
|
statusAttribute: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private byColumn: ColumnMap | undefined,
|
private byColumn: ColumnMap | undefined,
|
||||||
public columns: string[],
|
public columns: string[],
|
||||||
private parentNote: FNote,
|
private parentNote: FNote,
|
||||||
private statusAttribute: string,
|
statusAttribute: string,
|
||||||
private viewConfig: BoardViewData,
|
private viewConfig: BoardViewData,
|
||||||
private saveConfig: (newConfig: BoardViewData) => void,
|
private saveConfig: (newConfig: BoardViewData) => void,
|
||||||
private setBranchIdToEdit: (branchId: string | undefined) => void
|
private setBranchIdToEdit: (branchId: string | undefined) => void
|
||||||
) {};
|
) {
|
||||||
|
this.isRelationMode = statusAttribute.startsWith("~");
|
||||||
|
|
||||||
|
if (statusAttribute.startsWith("~") || statusAttribute.startsWith("#")) {
|
||||||
|
statusAttribute = statusAttribute.substring(1);
|
||||||
|
}
|
||||||
|
this.statusAttribute = statusAttribute;
|
||||||
|
};
|
||||||
|
|
||||||
async createNewItem(column: string, title: string) {
|
async createNewItem(column: string, title: string) {
|
||||||
try {
|
try {
|
||||||
@@ -42,8 +53,12 @@ export default class BoardApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async changeColumn(noteId: string, newColumn: string) {
|
async changeColumn(noteId: string, newColumn: string) {
|
||||||
|
if (this.isRelationMode) {
|
||||||
|
await attributes.setRelation(noteId, this.statusAttribute, newColumn);
|
||||||
|
} else {
|
||||||
await attributes.setLabel(noteId, this.statusAttribute, newColumn);
|
await attributes.setLabel(noteId, this.statusAttribute, newColumn);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async addNewColumn(columnName: string) {
|
async addNewColumn(columnName: string) {
|
||||||
if (!columnName.trim()) {
|
if (!columnName.trim()) {
|
||||||
@@ -60,22 +75,20 @@ export default class BoardApi {
|
|||||||
|
|
||||||
// Add the new column to persisted data if it doesn't exist
|
// Add the new column to persisted data if it doesn't exist
|
||||||
const existingColumn = this.viewConfig.columns.find(col => col.value === columnName);
|
const existingColumn = this.viewConfig.columns.find(col => col.value === columnName);
|
||||||
if (!existingColumn) {
|
if (existingColumn) return false;
|
||||||
this.viewConfig.columns.push({ value: columnName });
|
this.viewConfig.columns.push({ value: columnName });
|
||||||
this.saveConfig(this.viewConfig);
|
this.saveConfig(this.viewConfig);
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeColumn(column: string) {
|
async removeColumn(column: string) {
|
||||||
// Remove the value from the notes.
|
// Remove the value from the notes.
|
||||||
const noteIds = this.byColumn?.get(column)?.map(item => item.note.noteId) || [];
|
const noteIds = this.byColumn?.get(column)?.map(item => item.note.noteId) || [];
|
||||||
await executeBulkActions(noteIds, [
|
|
||||||
{
|
|
||||||
name: "deleteLabel",
|
|
||||||
labelName: this.statusAttribute
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
const action: BulkAction = this.isRelationMode
|
||||||
|
? { name: "deleteRelation", relationName: this.statusAttribute }
|
||||||
|
: { name: "deleteLabel", labelName: this.statusAttribute }
|
||||||
|
await executeBulkActions(noteIds, [ action ]);
|
||||||
this.viewConfig.columns = (this.viewConfig.columns ?? []).filter(col => col.value !== column);
|
this.viewConfig.columns = (this.viewConfig.columns ?? []).filter(col => col.value !== column);
|
||||||
this.saveConfig(this.viewConfig);
|
this.saveConfig(this.viewConfig);
|
||||||
}
|
}
|
||||||
@@ -84,13 +97,10 @@ export default class BoardApi {
|
|||||||
const noteIds = this.byColumn?.get(oldValue)?.map(item => item.note.noteId) || [];
|
const noteIds = this.byColumn?.get(oldValue)?.map(item => item.note.noteId) || [];
|
||||||
|
|
||||||
// Change the value in the notes.
|
// Change the value in the notes.
|
||||||
await executeBulkActions(noteIds, [
|
const action: BulkAction = this.isRelationMode
|
||||||
{
|
? { name: "updateRelationTarget", relationName: this.statusAttribute, targetNoteId: newValue }
|
||||||
name: "updateLabelValue",
|
: { name: "updateLabelValue", labelName: this.statusAttribute, labelValue: newValue }
|
||||||
labelName: this.statusAttribute,
|
await executeBulkActions(noteIds, [ action ]);
|
||||||
labelValue: newValue
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Rename the column in the persisted data.
|
// Rename the column in the persisted data.
|
||||||
for (const column of this.viewConfig.columns || []) {
|
for (const column of this.viewConfig.columns || []) {
|
||||||
@@ -167,8 +177,12 @@ export default class BoardApi {
|
|||||||
removeFromBoard(noteId: string) {
|
removeFromBoard(noteId: string) {
|
||||||
const note = froca.getNoteFromCache(noteId);
|
const note = froca.getNoteFromCache(noteId);
|
||||||
if (!note) return;
|
if (!note) return;
|
||||||
|
if (this.isRelationMode) {
|
||||||
|
return attributes.removeOwnedRelationByName(note, this.statusAttribute);
|
||||||
|
} else {
|
||||||
return attributes.removeOwnedLabelByName(note, this.statusAttribute);
|
return attributes.removeOwnedLabelByName(note, this.statusAttribute);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async moveWithinBoard(noteId: string, sourceBranchId: string, sourceIndex: number, targetIndex: number, sourceColumn: string, targetColumn: string) {
|
async moveWithinBoard(noteId: string, sourceBranchId: string, sourceIndex: number, targetIndex: number, sourceColumn: string, targetColumn: string) {
|
||||||
const targetItems = this.byColumn?.get(targetColumn) ?? [];
|
const targetItems = this.byColumn?.get(targetColumn) ?? [];
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { BoardViewContext, TitleEditor } from ".";
|
|||||||
import { ContextMenuEvent } from "../../../menus/context_menu";
|
import { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
import { openNoteContextMenu } from "./context_menu";
|
import { openNoteContextMenu } from "./context_menu";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
import UserAttributesDisplay from "../../attribute_widgets/UserAttributesList";
|
||||||
|
import { useTriliumEvent } from "../../react/hooks";
|
||||||
|
|
||||||
export const CARD_CLIPBOARD_TYPE = "trilium/board-card";
|
export const CARD_CLIPBOARD_TYPE = "trilium/board-card";
|
||||||
|
|
||||||
@@ -39,6 +41,13 @@ export default function Card({
|
|||||||
const [ isVisible, setVisible ] = useState(true);
|
const [ isVisible, setVisible ] = useState(true);
|
||||||
const [ title, setTitle ] = useState(note.title);
|
const [ title, setTitle ] = useState(note.title);
|
||||||
|
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
const row = loadResults.getEntityRow("notes", note.noteId);
|
||||||
|
if (row) {
|
||||||
|
setTitle(row.title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: DragEvent) => {
|
const handleDragStart = useCallback((e: DragEvent) => {
|
||||||
e.dataTransfer!.effectAllowed = 'move';
|
e.dataTransfer!.effectAllowed = 'move';
|
||||||
const data: CardDragData = { noteId: note.noteId, branchId: branch.branchId, fromColumn: column, index };
|
const data: CardDragData = { noteId: note.noteId, branchId: branch.branchId, fromColumn: column, index };
|
||||||
@@ -108,6 +117,7 @@ export default function Card({
|
|||||||
title={t("board_view.edit-note-title")}
|
title={t("board_view.edit-note-title")}
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
/>
|
/>
|
||||||
|
<UserAttributesDisplay note={note} ignoredAttributes={[api.statusAttribute]} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<TitleEditor
|
<TitleEditor
|
||||||
@@ -117,7 +127,7 @@ export default function Card({
|
|||||||
setTitle(newTitle);
|
setTitle(newTitle);
|
||||||
}}
|
}}
|
||||||
dismiss={() => api.dismissEditingTitle()}
|
dismiss={() => api.dismissEditingTitle()}
|
||||||
multiline
|
mode="multiline"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Card, { CARD_CLIPBOARD_TYPE, CardDragData } from "./card";
|
|||||||
import { JSX } from "preact/jsx-runtime";
|
import { JSX } from "preact/jsx-runtime";
|
||||||
import froca from "../../../services/froca";
|
import froca from "../../../services/froca";
|
||||||
import { DragData, TREE_CLIPBOARD_TYPE } from "../../note_tree";
|
import { DragData, TREE_CLIPBOARD_TYPE } from "../../note_tree";
|
||||||
|
import NoteLink from "../../react/NoteLink";
|
||||||
|
|
||||||
interface DragContext {
|
interface DragContext {
|
||||||
column: string;
|
column: string;
|
||||||
@@ -27,12 +28,14 @@ export default function Column({
|
|||||||
api,
|
api,
|
||||||
onColumnHover,
|
onColumnHover,
|
||||||
isAnyColumnDragging,
|
isAnyColumnDragging,
|
||||||
|
isInRelationMode
|
||||||
}: {
|
}: {
|
||||||
columnItems?: { note: FNote, branch: FBranch }[];
|
columnItems?: { note: FNote, branch: FBranch }[];
|
||||||
isDraggingColumn: boolean,
|
isDraggingColumn: boolean,
|
||||||
api: BoardApi,
|
api: BoardApi,
|
||||||
onColumnHover?: (index: number, mouseX: number, rect: DOMRect) => void,
|
onColumnHover?: (index: number, mouseX: number, rect: DOMRect) => void,
|
||||||
isAnyColumnDragging?: boolean
|
isAnyColumnDragging?: boolean,
|
||||||
|
isInRelationMode: boolean
|
||||||
} & DragContext) {
|
} & DragContext) {
|
||||||
const [ isVisible, setVisible ] = useState(true);
|
const [ isVisible, setVisible ] = useState(true);
|
||||||
const { columnNameToEdit, setColumnNameToEdit, dropTarget, draggedCard, dropPosition } = useContext(BoardViewContext)!;
|
const { columnNameToEdit, setColumnNameToEdit, dropTarget, draggedCard, dropPosition } = useContext(BoardViewContext)!;
|
||||||
@@ -103,7 +106,13 @@ export default function Column({
|
|||||||
>
|
>
|
||||||
{!isEditing ? (
|
{!isEditing ? (
|
||||||
<>
|
<>
|
||||||
<span className="title">{column}</span>
|
<span className="title">
|
||||||
|
{isInRelationMode
|
||||||
|
? <NoteLink notePath={column} showNoteIcon />
|
||||||
|
: column}
|
||||||
|
</span>
|
||||||
|
<span className="counter-badge">{columnItems?.length ?? 0}</span>
|
||||||
|
<div className="spacer" />
|
||||||
<span
|
<span
|
||||||
className="edit-icon icon bx bx-edit-alt"
|
className="edit-icon icon bx bx-edit-alt"
|
||||||
title={t("board_view.edit-column-title")}
|
title={t("board_view.edit-column-title")}
|
||||||
@@ -115,6 +124,7 @@ export default function Column({
|
|||||||
currentValue={column}
|
currentValue={column}
|
||||||
save={newTitle => api.renameColumn(column, newTitle)}
|
save={newTitle => api.renameColumn(column, newTitle)}
|
||||||
dismiss={() => setColumnNameToEdit?.(undefined)}
|
dismiss={() => setColumnNameToEdit?.(undefined)}
|
||||||
|
mode={isInRelationMode ? "relation" : "normal"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -178,7 +188,7 @@ function AddNewItem({ column, api }: { column: string, api: BoardApi }) {
|
|||||||
placeholder={t("board_view.new-item-placeholder")}
|
placeholder={t("board_view.new-item-placeholder")}
|
||||||
save={(title) => api.createNewItem(column, title)}
|
save={(title) => api.createNewItem(column, title)}
|
||||||
dismiss={() => setIsCreatingNewItem(false)}
|
dismiss={() => setIsCreatingNewItem(false)}
|
||||||
multiline isNewItem
|
mode="multiline" isNewItem
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
byColumn,
|
byColumn,
|
||||||
newPersistedData
|
newPersistedData,
|
||||||
|
isInRelationMode: groupByColumn.startsWith("~")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupB
|
|||||||
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived, seenNoteIds);
|
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived, seenNoteIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = note.getLabelValue(groupByColumn);
|
const group = note.getLabelOrRelation(groupByColumn);
|
||||||
if (!group || seenNoteIds.has(note.noteId)) {
|
if (!group || seenNoteIds.has(note.noteId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
--card-padding: 0.6em;
|
--card-padding: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.mobile .board-view {
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
.board-view-container {
|
.board-view-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -31,6 +37,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.mobile .board-view-container .board-column {
|
||||||
|
width: 75vw;
|
||||||
|
max-width: 300px;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.board-view-container .board-column.drag-over {
|
.board-view-container .board-column.drag-over {
|
||||||
border-color: var(--main-text-color);
|
border-color: var(--main-text-color);
|
||||||
background-color: var(--hover-item-background-color);
|
background-color: var(--hover-item-background-color);
|
||||||
@@ -53,7 +65,21 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-view-container .board-column h3 > .title {
|
.board-view-container .board-column h3 a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-view-container .board-column h3 .counter-badge {
|
||||||
|
background-color: var(--muted-text-color);
|
||||||
|
color: var(--main-background-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.1em 0.6em;
|
||||||
|
font-size: 0.75em;
|
||||||
|
margin-inline-start: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-view-container .board-column h3 > .spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import Column from "./column";
|
|||||||
import BoardApi from "./api";
|
import BoardApi from "./api";
|
||||||
import FormTextArea from "../../react/FormTextArea";
|
import FormTextArea from "../../react/FormTextArea";
|
||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
|
import NoteAutocomplete from "../../react/NoteAutocomplete";
|
||||||
|
import toast from "../../../services/toast";
|
||||||
|
|
||||||
export interface BoardViewData {
|
export interface BoardViewData {
|
||||||
columns?: BoardColumnData[];
|
columns?: BoardColumnData[];
|
||||||
@@ -42,10 +44,11 @@ interface BoardViewContextData {
|
|||||||
export const BoardViewContext = createContext<BoardViewContextData | undefined>(undefined);
|
export const BoardViewContext = createContext<BoardViewContextData | undefined>(undefined);
|
||||||
|
|
||||||
export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps<BoardViewData>) {
|
export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps<BoardViewData>) {
|
||||||
const [ statusAttribute ] = useNoteLabelWithDefault(parentNote, "board:groupBy", "status");
|
const [ statusAttributeWithPrefix ] = useNoteLabelWithDefault(parentNote, "board:groupBy", "status");
|
||||||
const [ includeArchived ] = useNoteLabelBoolean(parentNote, "includeArchived");
|
const [ includeArchived ] = useNoteLabelBoolean(parentNote, "includeArchived");
|
||||||
const [ byColumn, setByColumn ] = useState<ColumnMap>();
|
const [ byColumn, setByColumn ] = useState<ColumnMap>();
|
||||||
const [ columns, setColumns ] = useState<string[]>();
|
const [ columns, setColumns ] = useState<string[]>();
|
||||||
|
const [ isInRelationMode, setIsRelationMode ] = useState(false);
|
||||||
const [ draggedCard, setDraggedCard ] = useState<{ noteId: string, branchId: string, fromColumn: string, index: number } | null>(null);
|
const [ draggedCard, setDraggedCard ] = useState<{ noteId: string, branchId: string, fromColumn: string, index: number } | null>(null);
|
||||||
const [ dropTarget, setDropTarget ] = useState<string | null>(null);
|
const [ dropTarget, setDropTarget ] = useState<string | null>(null);
|
||||||
const [ dropPosition, setDropPosition ] = useState<{ column: string, index: number } | null>(null);
|
const [ dropPosition, setDropPosition ] = useState<{ column: string, index: number } | null>(null);
|
||||||
@@ -55,8 +58,8 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
|||||||
const [ branchIdToEdit, setBranchIdToEdit ] = useState<string>();
|
const [ branchIdToEdit, setBranchIdToEdit ] = useState<string>();
|
||||||
const [ columnNameToEdit, setColumnNameToEdit ] = useState<string>();
|
const [ columnNameToEdit, setColumnNameToEdit ] = useState<string>();
|
||||||
const api = useMemo(() => {
|
const api = useMemo(() => {
|
||||||
return new Api(byColumn, columns ?? [], parentNote, statusAttribute, viewConfig ?? {}, saveConfig, setBranchIdToEdit );
|
return new Api(byColumn, columns ?? [], parentNote, statusAttributeWithPrefix, viewConfig ?? {}, saveConfig, setBranchIdToEdit );
|
||||||
}, [ byColumn, columns, parentNote, statusAttribute, viewConfig, saveConfig, setBranchIdToEdit ]);
|
}, [ byColumn, columns, parentNote, statusAttributeWithPrefix, viewConfig, saveConfig, setBranchIdToEdit ]);
|
||||||
const boardViewContext = useMemo<BoardViewContextData>(() => ({
|
const boardViewContext = useMemo<BoardViewContextData>(() => ({
|
||||||
api,
|
api,
|
||||||
parentNote,
|
parentNote,
|
||||||
@@ -78,8 +81,9 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
getBoardData(parentNote, statusAttribute, viewConfig ?? {}, includeArchived).then(({ byColumn, newPersistedData }) => {
|
getBoardData(parentNote, statusAttributeWithPrefix, viewConfig ?? {}, includeArchived).then(({ byColumn, newPersistedData, isInRelationMode }) => {
|
||||||
setByColumn(byColumn);
|
setByColumn(byColumn);
|
||||||
|
setIsRelationMode(isInRelationMode);
|
||||||
|
|
||||||
if (newPersistedData) {
|
if (newPersistedData) {
|
||||||
viewConfig = { ...newPersistedData };
|
viewConfig = { ...newPersistedData };
|
||||||
@@ -94,7 +98,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(refresh, [ parentNote, noteIds, viewConfig ]);
|
useEffect(refresh, [ parentNote, noteIds, viewConfig, statusAttributeWithPrefix ]);
|
||||||
|
|
||||||
const handleColumnDrop = useCallback((fromIndex: number, toIndex: number) => {
|
const handleColumnDrop = useCallback((fromIndex: number, toIndex: number) => {
|
||||||
const newColumns = api.reorderColumn(fromIndex, toIndex);
|
const newColumns = api.reorderColumn(fromIndex, toIndex);
|
||||||
@@ -110,7 +114,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
|||||||
// Check if any changes affect our board
|
// Check if any changes affect our board
|
||||||
const hasRelevantChanges =
|
const hasRelevantChanges =
|
||||||
// React to changes in status attribute for notes in this board
|
// React to changes in status attribute for notes in this board
|
||||||
loadResults.getAttributeRows().some(attr => attr.name === statusAttribute && noteIds.includes(attr.noteId!)) ||
|
loadResults.getAttributeRows().some(attr => attr.name === api.statusAttribute && noteIds.includes(attr.noteId!)) ||
|
||||||
// React to changes in note title
|
// React to changes in note title
|
||||||
loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) ||
|
loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) ||
|
||||||
// React to changes in branches for subchildren (e.g., moved, added, or removed notes)
|
// React to changes in branches for subchildren (e.g., moved, added, or removed notes)
|
||||||
@@ -171,6 +175,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
|||||||
<div className="column-drop-placeholder show" />
|
<div className="column-drop-placeholder show" />
|
||||||
)}
|
)}
|
||||||
<Column
|
<Column
|
||||||
|
isInRelationMode={isInRelationMode}
|
||||||
api={api}
|
api={api}
|
||||||
column={column}
|
column={column}
|
||||||
columnIndex={index}
|
columnIndex={index}
|
||||||
@@ -185,14 +190,14 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
|||||||
<div className="column-drop-placeholder show" />
|
<div className="column-drop-placeholder show" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AddNewColumn api={api} />
|
<AddNewColumn api={api} isInRelationMode={isInRelationMode} />
|
||||||
</div>
|
</div>
|
||||||
</BoardViewContext.Provider>
|
</BoardViewContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddNewColumn({ api }: { api: BoardApi }) {
|
function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMode: boolean }) {
|
||||||
const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false);
|
const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false);
|
||||||
|
|
||||||
const addColumnCallback = useCallback(() => {
|
const addColumnCallback = useCallback(() => {
|
||||||
@@ -209,22 +214,28 @@ function AddNewColumn({ api }: { api: BoardApi }) {
|
|||||||
: (
|
: (
|
||||||
<TitleEditor
|
<TitleEditor
|
||||||
placeholder={t("board_view.add-column-placeholder")}
|
placeholder={t("board_view.add-column-placeholder")}
|
||||||
save={(columnName) => api.addNewColumn(columnName)}
|
save={async (columnName) => {
|
||||||
|
const created = await api.addNewColumn(columnName);
|
||||||
|
if (!created) {
|
||||||
|
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
|
||||||
|
}
|
||||||
|
}}
|
||||||
dismiss={() => setIsCreatingNewColumn(false)}
|
dismiss={() => setIsCreatingNewColumn(false)}
|
||||||
isNewItem
|
isNewItem
|
||||||
|
mode={isInRelationMode ? "relation" : "normal"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TitleEditor({ currentValue, placeholder, save, dismiss, multiline, isNewItem }: {
|
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
|
||||||
currentValue?: string;
|
currentValue?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
save: (newValue: string) => void;
|
save: (newValue: string) => void;
|
||||||
dismiss: () => void;
|
dismiss: () => void;
|
||||||
multiline?: boolean;
|
|
||||||
isNewItem?: boolean;
|
isNewItem?: boolean;
|
||||||
|
mode?: "normal" | "multiline" | "relation";
|
||||||
}) {
|
}) {
|
||||||
const inputRef = useRef<any>(null);
|
const inputRef = useRef<any>(null);
|
||||||
const focusElRef = useRef<Element>(null);
|
const focusElRef = useRef<Element>(null);
|
||||||
@@ -232,13 +243,11 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, multilin
|
|||||||
const shouldDismiss = useRef(false);
|
const shouldDismiss = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
focusElRef.current = document.activeElement;
|
focusElRef.current = document.activeElement !== document.body ? document.activeElement : null;
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
inputRef.current?.select();
|
inputRef.current?.select();
|
||||||
}, [ inputRef ]);
|
}, [ inputRef ]);
|
||||||
|
|
||||||
const Element = multiline ? FormTextArea : FormTextBox;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dismissOnNextRefreshRef.current) {
|
if (dismissOnNextRefreshRef.current) {
|
||||||
dismiss();
|
dismiss();
|
||||||
@@ -246,31 +255,62 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, multilin
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
const onKeyDown = (e: TargetedKeyboardEvent<HTMLInputElement | HTMLTextAreaElement> | KeyboardEvent) => {
|
||||||
<Element
|
|
||||||
inputRef={inputRef}
|
|
||||||
currentValue={currentValue ?? ""}
|
|
||||||
placeholder={placeholder}
|
|
||||||
autoComplete="trilium-title-entry" // forces the auto-fill off better than the "off" value.
|
|
||||||
rows={multiline ? 4 : undefined}
|
|
||||||
onKeyDown={(e: TargetedKeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === "Enter" || e.key === "Escape") {
|
if (e.key === "Enter" || e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
shouldDismiss.current = (e.key === "Escape");
|
|
||||||
if (focusElRef.current instanceof HTMLElement) {
|
if (focusElRef.current instanceof HTMLElement) {
|
||||||
|
shouldDismiss.current = (e.key === "Escape");
|
||||||
focusElRef.current.focus();
|
focusElRef.current.focus();
|
||||||
|
} else {
|
||||||
|
dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
};
|
||||||
onBlur={(newValue) => {
|
|
||||||
|
const onBlur = (newValue: string) => {
|
||||||
if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) {
|
if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) {
|
||||||
save(newValue);
|
save(newValue);
|
||||||
dismissOnNextRefreshRef.current = true;
|
dismissOnNextRefreshRef.current = true;
|
||||||
} else {
|
} else {
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode !== "relation") {
|
||||||
|
const Element = mode === "multiline" ? FormTextArea : FormTextBox;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Element
|
||||||
|
inputRef={inputRef}
|
||||||
|
currentValue={currentValue ?? ""}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoComplete="trilium-title-entry" // forces the auto-fill off better than the "off" value.
|
||||||
|
rows={mode === "multiline" ? 4 : undefined}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<NoteAutocomplete
|
||||||
|
inputRef={inputRef}
|
||||||
|
noteId={currentValue ?? ""}
|
||||||
|
opts={{
|
||||||
|
hideAllButtons: true,
|
||||||
|
allowCreatingNotes: true
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => dismiss()}
|
||||||
|
noteIdChanged={(newValue) => {
|
||||||
|
save(newValue);
|
||||||
|
dismiss();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import FNote from "../../../entities/fnote";
|
|||||||
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
import link_context_menu from "../../../menus/link_context_menu";
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
import branches from "../../../services/branches";
|
import branches from "../../../services/branches";
|
||||||
|
import froca from "../../../services/froca";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
||||||
@@ -18,8 +19,20 @@ export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, par
|
|||||||
title: t("calendar_view.delete_note"),
|
title: t("calendar_view.delete_note"),
|
||||||
uiIcon: "bx bx-trash",
|
uiIcon: "bx bx-trash",
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
const branchId = parentNote.childToBranch[noteId];
|
const noteToDelete = await froca.getNote(noteId);
|
||||||
await branches.deleteNotes([ branchId ], false, false);
|
if (!noteToDelete) return;
|
||||||
|
|
||||||
|
let branchIdToDelete: string | null = null;
|
||||||
|
for (const parentBranch of noteToDelete.getParentBranches()) {
|
||||||
|
const parentNote = await parentBranch.getNote();
|
||||||
|
if (parentNote?.hasAncestor(parentNote.noteId)) {
|
||||||
|
branchIdToDelete = parentBranch.branchId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branchIdToDelete) {
|
||||||
|
await branches.deleteNotes([ branchIdToDelete ], false, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
|||||||
const [ hideWeekends ] = useNoteLabelBoolean(note, "calendar:hideWeekends");
|
const [ hideWeekends ] = useNoteLabelBoolean(note, "calendar:hideWeekends");
|
||||||
const [ weekNumbers ] = useNoteLabelBoolean(note, "calendar:weekNumbers");
|
const [ weekNumbers ] = useNoteLabelBoolean(note, "calendar:weekNumbers");
|
||||||
const [ calendarView, setCalendarView ] = useNoteLabel(note, "calendar:view");
|
const [ calendarView, setCalendarView ] = useNoteLabel(note, "calendar:view");
|
||||||
|
const [ initialDate ] = useNoteLabel(note, "calendar:initialDate");
|
||||||
const initialView = useRef(calendarView);
|
const initialView = useRef(calendarView);
|
||||||
const viewSpacedUpdate = useSpacedUpdate(() => setCalendarView(initialView.current));
|
const viewSpacedUpdate = useSpacedUpdate(() => setCalendarView(initialView.current));
|
||||||
useResizeObserver(containerRef, () => calendarRef.current?.updateSize());
|
useResizeObserver(containerRef, () => calendarRef.current?.updateSize());
|
||||||
@@ -134,6 +135,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
|||||||
height="90%"
|
height="90%"
|
||||||
nowIndicator
|
nowIndicator
|
||||||
handleWindowResize={false}
|
handleWindowResize={false}
|
||||||
|
initialDate={initialDate || undefined}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
{...editingProps}
|
{...editingProps}
|
||||||
eventDidMount={eventDidMount}
|
eventDidMount={eventDidMount}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-book-card.archived {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.note-book-card:not(.expanded) .note-book-content {
|
.note-book-card:not(.expanded) .note-book-content {
|
||||||
padding: 10px
|
padding: 10px
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function ListNoteCard({ note, parentNote, expand, highlightedTokens }: { note: F
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`note-book-card no-tooltip-preview ${isExpanded ? "expanded" : ""}`}
|
className={`note-book-card no-tooltip-preview ${isExpanded ? "expanded" : ""} ${note.isArchived ? "archived" : ""}`}
|
||||||
data-note-id={note.noteId}
|
data-note-id={note.noteId}
|
||||||
>
|
>
|
||||||
<h5 className="note-book-header">
|
<h5 className="note-book-header">
|
||||||
@@ -100,7 +100,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`note-book-card no-tooltip-preview block-link`}
|
className={`note-book-card no-tooltip-preview block-link ${note.isArchived ? "archived" : ""}`}
|
||||||
data-href={`#${notePath}`}
|
data-href={`#${notePath}`}
|
||||||
data-note-id={note.noteId}
|
data-note-id={note.noteId}
|
||||||
onClick={(e) => link.goToLink(e)}
|
onClick={(e) => link.goToLink(e)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default class ContentHeader extends Container<BasicWidget> {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.class("content-header-widget");
|
||||||
this.css("contain", "unset");
|
this.css("contain", "unset");
|
||||||
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
|
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
|
||||||
}
|
}
|
||||||
@@ -100,9 +100,23 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async closeThisNoteSplitCommand({ ntxId }: CommandListenerData<"closeThisNoteSplit">) {
|
async closeThisNoteSplitCommand({ ntxId }: CommandListenerData<"closeThisNoteSplit">) {
|
||||||
if (ntxId) {
|
if (!ntxId) return;
|
||||||
await appContext.tabManager.removeNoteContext(ntxId);
|
const contexts = appContext.tabManager.noteContexts;
|
||||||
|
|
||||||
|
const currentIndex = contexts.findIndex((c) => c.ntxId === ntxId);
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
const isRemoveMainContext = !contexts[currentIndex].mainNtxId;
|
||||||
|
if (isRemoveMainContext && currentIndex + 1 <= contexts.length) {
|
||||||
|
const ntxIds = contexts.map((c) => c.ntxId).filter((c) => !!c) as string[];
|
||||||
|
this.triggerCommand("noteContextReorder", {
|
||||||
|
ntxIdsInOrder: ntxIds,
|
||||||
|
oldMainNtxId: ntxId,
|
||||||
|
newMainNtxId: ntxIds[currentIndex + 1]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await appContext.tabManager.removeNoteContext(ntxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveThisNoteSplitCommand({ ntxId, isMovingLeft }: CommandListenerData<"moveThisNoteSplit">) {
|
async moveThisNoteSplitCommand({ ntxId, isMovingLeft }: CommandListenerData<"moveThisNoteSplit">) {
|
||||||
@@ -167,12 +181,16 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
|||||||
splitService.delNoteSplitResizer(ntxIds);
|
splitService.delNoteSplitResizer(ntxIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
contextsReopenedEvent({ ntxId, afterNtxId }: EventData<"contextsReopened">) {
|
contextsReopenedEvent({ ntxId, mainNtxId, tabPosition, afterNtxId }: EventData<"contextsReopened">) {
|
||||||
if (ntxId === undefined || afterNtxId === undefined) {
|
if (ntxId !== undefined && afterNtxId !== undefined) {
|
||||||
// no single split reopened
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$widget.find(`[data-ntx-id="${ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${afterNtxId}"]`));
|
this.$widget.find(`[data-ntx-id="${ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${afterNtxId}"]`));
|
||||||
|
} else if (mainNtxId && tabPosition >= 0) {
|
||||||
|
const contexts = appContext.tabManager.noteContexts;
|
||||||
|
const nextIndex = contexts.findIndex(c => c.ntxId === mainNtxId);
|
||||||
|
const beforeNtxId = (nextIndex !== -1 && nextIndex + 1 < contexts.length) ? contexts[nextIndex + 1].ntxId : null;
|
||||||
|
|
||||||
|
this.$widget.find(`[data-ntx-id="${mainNtxId}"]`).insertBefore(this.$widget.find(`[data-ntx-id="${beforeNtxId}"]`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ interface NoteAutocompleteProps {
|
|||||||
opts?: Omit<Options, "container">;
|
opts?: Omit<Options, "container">;
|
||||||
onChange?: (suggestion: Suggestion | null) => void;
|
onChange?: (suggestion: Suggestion | null) => void;
|
||||||
onTextChange?: (text: string) => void;
|
onTextChange?: (text: string) => void;
|
||||||
|
onKeyDown?: (e: KeyboardEvent) => void;
|
||||||
|
onBlur?: (newValue: string) => void;
|
||||||
noteIdChanged?: (noteId: string) => void;
|
noteIdChanged?: (noteId: string) => void;
|
||||||
noteId?: string;
|
noteId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
|
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged, onKeyDown, onBlur }: NoteAutocompleteProps) {
|
||||||
const ref = useSyncedRef<HTMLInputElement>(externalInputRef);
|
const ref = useSyncedRef<HTMLInputElement>(externalInputRef);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,6 +59,12 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
|
|||||||
if (onTextChange) {
|
if (onTextChange) {
|
||||||
$autoComplete.on("input", () => onTextChange($autoComplete[0].value));
|
$autoComplete.on("input", () => onTextChange($autoComplete[0].value));
|
||||||
}
|
}
|
||||||
|
if (onKeyDown) {
|
||||||
|
$autoComplete.on("keydown", (e) => e.originalEvent && onKeyDown(e.originalEvent));
|
||||||
|
}
|
||||||
|
if (onBlur) {
|
||||||
|
$autoComplete.on("blur", () => onBlur($autoComplete.getSelectedNoteId() ?? ""));
|
||||||
|
}
|
||||||
}, [opts, container?.current]);
|
}, [opts, container?.current]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import link, { ViewScope } from "../../services/link";
|
import link, { ViewScope } from "../../services/link";
|
||||||
import { useImperativeSearchHighlighlighting } from "./hooks";
|
import { useImperativeSearchHighlighlighting, useTriliumEvent } from "./hooks";
|
||||||
|
|
||||||
interface NoteLinkOpts {
|
interface NoteLinkOpts {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -19,9 +19,11 @@ interface NoteLinkOpts {
|
|||||||
|
|
||||||
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
|
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
|
||||||
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
||||||
|
const noteId = stringifiedNotePath.split("/").at(-1);
|
||||||
const ref = useRef<HTMLSpanElement>(null);
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
||||||
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
||||||
|
const [ noteTitle, setNoteTitle ] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
link.createLink(stringifiedNotePath, {
|
link.createLink(stringifiedNotePath, {
|
||||||
@@ -30,7 +32,7 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
|
|||||||
showNoteIcon,
|
showNoteIcon,
|
||||||
viewScope
|
viewScope
|
||||||
}).then(setJqueryEl);
|
}).then(setJqueryEl);
|
||||||
}, [ stringifiedNotePath, showNotePath, title, viewScope ]);
|
}, [ stringifiedNotePath, showNotePath, title, viewScope, noteTitle ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !jqueryEl) return;
|
if (!ref.current || !jqueryEl) return;
|
||||||
@@ -38,6 +40,16 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
|
|||||||
highlightSearch(ref.current);
|
highlightSearch(ref.current);
|
||||||
}, [ jqueryEl, highlightedTokens ]);
|
}, [ jqueryEl, highlightedTokens ]);
|
||||||
|
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
// React to note title changes, but only if the title is not overwritten.
|
||||||
|
if (!title && noteId) {
|
||||||
|
const entityRow = loadResults.getEntityRow("notes", noteId);
|
||||||
|
if (entityRow) {
|
||||||
|
setNoteTitle(entityRow.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (style) {
|
if (style) {
|
||||||
jqueryEl?.css(style);
|
jqueryEl?.css(style);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,14 +406,17 @@ export function useNoteLabelWithDefault(note: FNote | undefined | null, labelNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: FilterLabelsByType<boolean>): [ boolean, (newValue: boolean) => void] {
|
export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: FilterLabelsByType<boolean>): [ boolean, (newValue: boolean) => void] {
|
||||||
const [ labelValue, setLabelValue ] = useState<boolean>(!!note?.hasLabel(labelName));
|
const [, forceRender] = useState({});
|
||||||
|
|
||||||
useEffect(() => setLabelValue(!!note?.hasLabel(labelName)), [ note ]);
|
useEffect(() => {
|
||||||
|
forceRender({});
|
||||||
|
}, [ note ]);
|
||||||
|
|
||||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
for (const attr of loadResults.getAttributeRows()) {
|
for (const attr of loadResults.getAttributeRows()) {
|
||||||
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
|
||||||
setLabelValue(!attr.isDeleted);
|
forceRender({});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -430,6 +433,7 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: F
|
|||||||
|
|
||||||
useDebugValue(labelName);
|
useDebugValue(labelName);
|
||||||
|
|
||||||
|
const labelValue = !!note?.hasLabel(labelName);
|
||||||
return [ labelValue, setter ] as const;
|
return [ labelValue, setter ] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOpti
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{viewType !== "list" && viewType !== "grid" && (
|
|
||||||
<CheckboxPropertyView
|
<CheckboxPropertyView
|
||||||
note={note} property={{
|
note={note} property={{
|
||||||
bindToLabel: "includeArchived",
|
bindToLabel: "includeArchived",
|
||||||
@@ -73,7 +72,6 @@ function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOpti
|
|||||||
type: "checkbox"
|
type: "checkbox"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
|||||||
await attributes.removeAttributeById(noteId, expandedAttr.attributeId);
|
await attributes.removeAttributeById(noteId, expandedAttr.attributeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerCommand("refreshNoteList", { noteId: noteId });
|
triggerCommand("refreshNoteList", { noteId });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -820,12 +820,15 @@ export default class TabRowWidget extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopened">) {
|
contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopened">) {
|
||||||
if (!mainNtxId || !tabPosition) {
|
if (!mainNtxId || tabPosition < 0) {
|
||||||
// no tab reopened
|
// no tab reopened
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tabEl = this.getTabById(mainNtxId)[0];
|
const tabEl = this.getTabById(mainNtxId)[0];
|
||||||
tabEl.parentNode?.insertBefore(tabEl, this.tabEls[tabPosition]);
|
|
||||||
|
if ( tabEl && tabEl.parentNode ){
|
||||||
|
tabEl.parentNode.insertBefore(tabEl, this.tabEls[tabPosition]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTabById(ntxId: string | null) {
|
updateTabById(ntxId: string | null) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useTriliumEvent } from "../react/hooks";
|
|||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
|
|
||||||
export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
|
export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
|
||||||
const [ html, setHtml ] = useState<string>();
|
|
||||||
const initialized = useRef<Promise<void> | null>(null);
|
const initialized = useRef<Promise<void> | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
|
|||||||
if (!note) return;
|
if (!note) return;
|
||||||
|
|
||||||
initialized.current = renderDoc(note).then($content => {
|
initialized.current = renderDoc(note).then($content => {
|
||||||
setHtml($content.html());
|
containerRef.current?.replaceChildren(...$content);
|
||||||
});
|
});
|
||||||
}, [ note ]);
|
}, [ note ]);
|
||||||
|
|
||||||
@@ -26,10 +25,9 @@ export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RawHtmlBlock
|
<div
|
||||||
containerRef={containerRef}
|
ref={containerRef}
|
||||||
className={`note-detail-doc-content ck-content ${viewScope?.viewMode === "contextual-help" ? "contextual-help" : ""}`}
|
className={`note-detail-doc-content ck-content ${viewScope?.viewMode === "contextual-help" ? "contextual-help" : ""}`}
|
||||||
html={html}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useCallback, useEffect, useRef } from "preact/hooks";
|
import { useCallback, useEffect, useRef } from "preact/hooks";
|
||||||
import { TypeWidgetProps } from "./type_widget";
|
import { TypeWidgetProps } from "./type_widget";
|
||||||
import { MindElixirData, MindElixirInstance, Operation, default as VanillaMindElixir } from "mind-elixir";
|
import { MindElixirData, MindElixirInstance, Operation, Options, default as VanillaMindElixir } from "mind-elixir";
|
||||||
import { HTMLAttributes, RefObject } from "preact";
|
import { HTMLAttributes, RefObject } from "preact";
|
||||||
// allow node-menu plugin css to be bundled by webpack
|
// allow node-menu plugin css to be bundled by webpack
|
||||||
import nodeMenu from "@mind-elixir/node-menu";
|
import nodeMenu from "@mind-elixir/node-menu";
|
||||||
import "mind-elixir/style";
|
import "mind-elixir/style";
|
||||||
import "@mind-elixir/node-menu/dist/style.css";
|
import "@mind-elixir/node-menu/dist/style.css";
|
||||||
import "./MindMap.css";
|
import "./MindMap.css";
|
||||||
import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents } from "../react/hooks";
|
import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
import utils from "../../services/utils";
|
import utils from "../../services/utils";
|
||||||
|
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||||
|
|
||||||
const NEW_TOPIC_NAME = "";
|
const NEW_TOPIC_NAME = "";
|
||||||
|
|
||||||
@@ -21,6 +22,24 @@ interface MindElixirProps {
|
|||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null> = {
|
||||||
|
ar: null,
|
||||||
|
cn: "zh_CN",
|
||||||
|
de: null,
|
||||||
|
en: "en",
|
||||||
|
en_rtl: "en",
|
||||||
|
es: "es",
|
||||||
|
fr: "fr",
|
||||||
|
it: "it",
|
||||||
|
ja: "ja",
|
||||||
|
pt: "pt",
|
||||||
|
pt_br: "pt",
|
||||||
|
ro: null,
|
||||||
|
ru: "ru",
|
||||||
|
tw: "zh_TW",
|
||||||
|
uk: null
|
||||||
|
};
|
||||||
|
|
||||||
export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
|
export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||||
const apiRef = useRef<MindElixirInstance>(null);
|
const apiRef = useRef<MindElixirInstance>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -110,12 +129,14 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
|
|||||||
function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef: externalApiRef, onChange, editable }: MindElixirProps) {
|
function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef: externalApiRef, onChange, editable }: MindElixirProps) {
|
||||||
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
|
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
|
||||||
const apiRef = useRef<MindElixirInstance>(null);
|
const apiRef = useRef<MindElixirInstance>(null);
|
||||||
|
const [ locale ] = useTriliumOption("locale");
|
||||||
|
|
||||||
function reinitialize() {
|
function reinitialize() {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
const mind = new VanillaMindElixir({
|
const mind = new VanillaMindElixir({
|
||||||
el: containerRef.current,
|
el: containerRef.current,
|
||||||
|
locale: LOCALE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined,
|
||||||
editable
|
editable
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,7 +164,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
|
|||||||
if (data) {
|
if (data) {
|
||||||
apiRef.current?.init(data);
|
apiRef.current?.init(data);
|
||||||
}
|
}
|
||||||
}, [ editable ]);
|
}, [ editable, locale ]);
|
||||||
|
|
||||||
// On change listener.
|
// On change listener.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import { TypeWidgetProps } from "../type_widget";
|
import { TypeWidgetProps } from "../type_widget";
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
import { useNoteLabelBoolean } from "../../react/hooks";
|
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||||
import { useCallback, useMemo, useRef } from "preact/hooks";
|
import { useCallback, useMemo, useRef } from "preact/hooks";
|
||||||
import { type ExcalidrawImperativeAPI, type AppState } from "@excalidraw/excalidraw/types";
|
import { type ExcalidrawImperativeAPI, type AppState } from "@excalidraw/excalidraw/types";
|
||||||
import options from "../../../services/options";
|
import options from "../../../services/options";
|
||||||
@@ -9,6 +9,8 @@ import "./Canvas.css";
|
|||||||
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||||
import { goToLinkExt } from "../../../services/link";
|
import { goToLinkExt } from "../../../services/link";
|
||||||
import useCanvasPersistence from "./persistence";
|
import useCanvasPersistence from "./persistence";
|
||||||
|
import { LANGUAGE_MAPPINGS } from "./i18n";
|
||||||
|
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||||
|
|
||||||
// currently required by excalidraw, in order to allows self-hosting fonts locally.
|
// currently required by excalidraw, in order to allows self-hosting fonts locally.
|
||||||
// this avoids making excalidraw load the fonts from an external CDN.
|
// this avoids making excalidraw load the fonts from an external CDN.
|
||||||
@@ -21,6 +23,7 @@ export default function Canvas({ note, noteContext }: TypeWidgetProps) {
|
|||||||
const documentStyle = window.getComputedStyle(document.documentElement);
|
const documentStyle = window.getComputedStyle(document.documentElement);
|
||||||
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
|
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
|
||||||
}, []);
|
}, []);
|
||||||
|
const [ locale ] = useTriliumOption("locale");
|
||||||
const persistence = useCanvasPersistence(note, noteContext, apiRef, themeStyle, isReadOnly);
|
const persistence = useCanvasPersistence(note, noteContext, apiRef, themeStyle, isReadOnly);
|
||||||
|
|
||||||
/** Use excalidraw's native zoom instead of the global zoom. */
|
/** Use excalidraw's native zoom instead of the global zoom. */
|
||||||
@@ -58,6 +61,7 @@ export default function Canvas({ note, noteContext }: TypeWidgetProps) {
|
|||||||
detectScroll={false}
|
detectScroll={false}
|
||||||
handleKeyboardGlobally={false}
|
handleKeyboardGlobally={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
|
langCode={LANGUAGE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined}
|
||||||
UIOptions={{
|
UIOptions={{
|
||||||
canvasActions: {
|
canvasActions: {
|
||||||
saveToActiveFile: false,
|
saveToActiveFile: false,
|
||||||
|
|||||||
29
apps/client/src/widgets/type_widgets/canvas/i18n.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { LOCALES } from "@triliumnext/commons";
|
||||||
|
import { readdirSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { LANGUAGE_MAPPINGS } from "./i18n.js";
|
||||||
|
|
||||||
|
const localeDir = join(__dirname, "../../../../../../node_modules/@excalidraw/excalidraw/dist/prod/locales");
|
||||||
|
|
||||||
|
describe("Canvas i18n", () => {
|
||||||
|
it("all languages are mapped correctly", () => {
|
||||||
|
// Read the node_modules dir to obtain all the supported locales.
|
||||||
|
const supportedLanguageCodes = new Set<string>();
|
||||||
|
for (const file of readdirSync(localeDir)) {
|
||||||
|
if (file.startsWith("percentages")) continue;
|
||||||
|
const match = file.match("^[a-z]{2,3}(?:-[A-Z]{2,3})?");
|
||||||
|
if (!match) continue;
|
||||||
|
supportedLanguageCodes.add(match[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-check the locales.
|
||||||
|
for (const locale of LOCALES) {
|
||||||
|
if (locale.contentOnly || locale.devOnly) continue;
|
||||||
|
const languageCode = LANGUAGE_MAPPINGS[locale.id];
|
||||||
|
if (!supportedLanguageCodes.has(languageCode)) {
|
||||||
|
expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
19
apps/client/src/widgets/type_widgets/canvas/i18n.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||||
|
|
||||||
|
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, string | null> = {
|
||||||
|
ar: "ar-SA",
|
||||||
|
cn: "zh-CN",
|
||||||
|
de: "de-DE",
|
||||||
|
en: "en",
|
||||||
|
en_rtl: "en",
|
||||||
|
es: "es-ES",
|
||||||
|
fr: "fr-FR",
|
||||||
|
it: "it-IT",
|
||||||
|
ja: "ja-JP",
|
||||||
|
pt: "pt-PT",
|
||||||
|
pt_br: "pt-BR",
|
||||||
|
ro: "ro-RO",
|
||||||
|
ru: "ru-RU",
|
||||||
|
tw: "zh-TW",
|
||||||
|
uk: "uk-UA"
|
||||||
|
};
|
||||||
@@ -203,7 +203,7 @@ function Font({ title, fontFamilyOption, fontSizeOption }: { title: string, font
|
|||||||
<FormTextBoxWithUnit
|
<FormTextBoxWithUnit
|
||||||
name="tree-font-size"
|
name="tree-font-size"
|
||||||
type="number" min={50} max={200} step={10}
|
type="number" min={50} max={200} step={10}
|
||||||
currentValue={fontSize} onChange={setFontSize}
|
currentValue={fontSize} onBlur={setFontSize}
|
||||||
unit={t("units.percentage")}
|
unit={t("units.percentage")}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat";
|
import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat";
|
||||||
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor, TemplateDefinition } from "@triliumnext/ckeditor5";
|
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor, TemplateDefinition } from "@triliumnext/ckeditor5";
|
||||||
import { buildConfig, BuildEditorOptions } from "./config";
|
import { buildConfig, BuildEditorOptions } from "./config";
|
||||||
import { useLegacyImperativeHandlers, useSyncedRef } from "../../react/hooks";
|
import { useKeyboardShortcuts, useLegacyImperativeHandlers, useNoteContext, useSyncedRef, useTriliumOption } from "../../react/hooks";
|
||||||
import link from "../../../services/link";
|
import link from "../../../services/link";
|
||||||
import froca from "../../../services/froca";
|
import froca from "../../../services/froca";
|
||||||
|
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||||
|
|
||||||
export type BoxSize = "small" | "medium" | "full";
|
export type BoxSize = "small" | "medium" | "full";
|
||||||
|
|
||||||
@@ -37,7 +38,11 @@ interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "cla
|
|||||||
export default function CKEditorWithWatchdog({ containerRef: externalContainerRef, content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi, templates }: CKEditorWithWatchdogProps) {
|
export default function CKEditorWithWatchdog({ containerRef: externalContainerRef, content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi, templates }: CKEditorWithWatchdogProps) {
|
||||||
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
|
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
|
||||||
const watchdogRef = useRef<EditorWatchdog>(null);
|
const watchdogRef = useRef<EditorWatchdog>(null);
|
||||||
|
const [ uiLanguage ] = useTriliumOption("locale");
|
||||||
const [ editor, setEditor ] = useState<CKTextEditor>();
|
const [ editor, setEditor ] = useState<CKTextEditor>();
|
||||||
|
const { parentComponent } = useNoteContext();
|
||||||
|
|
||||||
|
useKeyboardShortcuts("text-detail", containerRef, parentComponent);
|
||||||
|
|
||||||
useImperativeHandle(editorApi, () => ({
|
useImperativeHandle(editorApi, () => ({
|
||||||
hasSelection() {
|
hasSelection() {
|
||||||
@@ -153,6 +158,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
|
|||||||
const editor = await buildEditor(container, !!isClassicEditor, {
|
const editor = await buildEditor(container, !!isClassicEditor, {
|
||||||
forceGplLicense: false,
|
forceGplLicense: false,
|
||||||
isClassicEditor: !!isClassicEditor,
|
isClassicEditor: !!isClassicEditor,
|
||||||
|
uiLanguage: uiLanguage as DISPLAYABLE_LOCALE_IDS,
|
||||||
contentLanguage: contentLanguage ?? null,
|
contentLanguage: contentLanguage ?? null,
|
||||||
templates
|
templates
|
||||||
});
|
});
|
||||||
@@ -177,7 +183,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
|
|||||||
watchdog.create(container);
|
watchdog.create(container);
|
||||||
|
|
||||||
return () => watchdog.destroy();
|
return () => watchdog.destroy();
|
||||||
}, [ contentLanguage, templates ]);
|
}, [ contentLanguage, templates, uiLanguage ]);
|
||||||
|
|
||||||
// React to content changes.
|
// React to content changes.
|
||||||
useEffect(() => editor?.setData(content ?? ""), [ editor, content ]);
|
useEffect(() => editor?.setData(content ?? ""), [ editor, content ]);
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ body.mobile .note-detail-editable-text {
|
|||||||
.note-detail-editable-text h5 { font-size: 1.1em; }
|
.note-detail-editable-text h5 { font-size: 1.1em; }
|
||||||
.note-detail-editable-text h6 { font-size: 1.0em; }
|
.note-detail-editable-text h6 { font-size: 1.0em; }
|
||||||
|
|
||||||
body.heading-style-markdown .note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-editable-text h2::before { content: "##\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-editable-text h3::before { content: "###\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-editable-text h4:not(.include-note-title)::before { content: "####\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-editable-text h5::before { content: "#####\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-editable-text h6::before { content: "######\2004"; color: var(--muted-text-color); }
|
||||||
|
|
||||||
body.heading-style-underline .note-detail-editable-text h2 { border-bottom: 1px solid var(--main-border-color); }
|
body.heading-style-underline .note-detail-editable-text h2 { border-bottom: 1px solid var(--main-border-color); }
|
||||||
body.heading-style-underline .note-detail-editable-text h3 { border-bottom: 1px solid var(--main-border-color); }
|
body.heading-style-underline .note-detail-editable-text h3 { border-bottom: 1px solid var(--main-border-color); }
|
||||||
|
|||||||
@@ -196,14 +196,12 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useKeyboardShortcuts("text-detail", containerRef, parentComponent);
|
|
||||||
useTriliumEvent("insertDateTimeToText", ({ ntxId: eventNtxId }) => {
|
useTriliumEvent("insertDateTimeToText", ({ ntxId: eventNtxId }) => {
|
||||||
if (eventNtxId !== ntxId) return;
|
if (eventNtxId !== ntxId) return;
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const customDateTimeFormat = options.get("customDateTimeFormat");
|
const customDateTimeFormat = options.get("customDateTimeFormat");
|
||||||
const dateString = utils.formatDateTime(date, customDateTimeFormat);
|
const dateString = utils.formatDateTime(date, customDateTimeFormat);
|
||||||
|
|
||||||
console.log("Insert text ", ntxId, eventNtxId, dateString);
|
|
||||||
addTextToEditor(dateString);
|
addTextToEditor(dateString);
|
||||||
});
|
});
|
||||||
useTriliumEvent("addTextToActiveEditor", ({ text }) => {
|
useTriliumEvent("addTextToActiveEditor", ({ text }) => {
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
.note-detail-readonly-text h5 { font-size: 1.1em; }
|
.note-detail-readonly-text h5 { font-size: 1.1em; }
|
||||||
.note-detail-readonly-text h6 { font-size: 1.0em; }
|
.note-detail-readonly-text h6 { font-size: 1.0em; }
|
||||||
|
|
||||||
body.heading-style-markdown .note-detail-readonly-text h1::before { content: "#\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h1::before { content: "#\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h2::before { content: "##\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h3::before { content: "###\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-readonly-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h5::before { content: "#####\2004"; color: var(--muted-text-color); }
|
||||||
body.heading-style-markdown .note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); }
|
body.heading-style-markdown .note-detail-readonly-text h6::before { content: "######\2004"; color: var(--muted-text-color); }
|
||||||
|
|
||||||
body.heading-style-underline .note-detail-readonly-text h1 { border-bottom: 1px solid var(--main-border-color); }
|
body.heading-style-underline .note-detail-readonly-text h1 { border-bottom: 1px solid var(--main-border-color); }
|
||||||
body.heading-style-underline .note-detail-readonly-text h2 { border-bottom: 1px solid var(--main-border-color); }
|
body.heading-style-underline .note-detail-readonly-text h2 { border-bottom: 1px solid var(--main-border-color); }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import link from "../../../services/link";
|
|||||||
import { formatCodeBlocks } from "../../../services/syntax_highlight";
|
import { formatCodeBlocks } from "../../../services/syntax_highlight";
|
||||||
import TouchBar, { TouchBarButton, TouchBarSpacer } from "../../react/TouchBar";
|
import TouchBar, { TouchBarButton, TouchBarSpacer } from "../../react/TouchBar";
|
||||||
import appContext from "../../../components/app_context";
|
import appContext from "../../../components/app_context";
|
||||||
|
import { applyReferenceLinks } from "./read_only_helper";
|
||||||
|
|
||||||
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {
|
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||||
const blob = useNoteBlob(note);
|
const blob = useNoteBlob(note);
|
||||||
@@ -122,10 +123,3 @@ function applyMath(container: HTMLDivElement) {
|
|||||||
renderMathInElement(equation, { trust: true });
|
renderMathInElement(equation, { trust: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyReferenceLinks(container: HTMLDivElement) {
|
|
||||||
const referenceLinks = container.querySelectorAll<HTMLDivElement>("a.reference-link");
|
|
||||||
for (const referenceLink of referenceLinks) {
|
|
||||||
link.loadReferenceLinkTitle($(referenceLink));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
39
apps/client/src/widgets/type_widgets/text/config.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { DISPLAYABLE_LOCALE_IDS, LOCALES } from "@triliumnext/commons";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock('../../../services/options.js', () => ({
|
||||||
|
default: {
|
||||||
|
get(name: string) {
|
||||||
|
if (name === "allowedHtmlTags") return "[]";
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
getJson: () => []
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("CK config", () => {
|
||||||
|
it("maps all languages correctly", async () => {
|
||||||
|
const { buildConfig } = await import("./config.js");
|
||||||
|
for (const locale of LOCALES) {
|
||||||
|
if (locale.contentOnly || locale.devOnly) continue;
|
||||||
|
|
||||||
|
const config = await buildConfig({
|
||||||
|
uiLanguage: locale.id as DISPLAYABLE_LOCALE_IDS,
|
||||||
|
contentLanguage: locale.id,
|
||||||
|
forceGplLicense: false,
|
||||||
|
isClassicEditor: false,
|
||||||
|
templates: []
|
||||||
|
});
|
||||||
|
|
||||||
|
let expectedLocale = locale.id.substring(0, 2);
|
||||||
|
if (expectedLocale === "cn") expectedLocale = "zh";
|
||||||
|
if (expectedLocale === "tw") expectedLocale = "zh-tw";
|
||||||
|
|
||||||
|
if (locale.id !== "en") {
|
||||||
|
expect((config.language as any).ui).toMatch(new RegExp(`^${expectedLocale}`));
|
||||||
|
expect(config.translations, locale.id).toBeDefined();
|
||||||
|
expect(config.translations, locale.id).toHaveLength(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ALLOWED_PROTOCOLS, MIME_TYPE_AUTO } from "@triliumnext/commons";
|
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO } from "@triliumnext/commons";
|
||||||
import { buildExtraCommands, type EditorConfig, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
|
import { buildExtraCommands, type EditorConfig, getCkLocale, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
|
||||||
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
||||||
import options from "../../../services/options.js";
|
import options from "../../../services/options.js";
|
||||||
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||||
@@ -17,6 +17,7 @@ export const OPEN_SOURCE_LICENSE_KEY = "GPL";
|
|||||||
export interface BuildEditorOptions {
|
export interface BuildEditorOptions {
|
||||||
forceGplLicense: boolean;
|
forceGplLicense: boolean;
|
||||||
isClassicEditor: boolean;
|
isClassicEditor: boolean;
|
||||||
|
uiLanguage: DISPLAYABLE_LOCALE_IDS;
|
||||||
contentLanguage: string | null;
|
contentLanguage: string | null;
|
||||||
templates: TemplateDefinition[];
|
templates: TemplateDefinition[];
|
||||||
}
|
}
|
||||||
@@ -161,9 +162,8 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
|||||||
htmlSupport: {
|
htmlSupport: {
|
||||||
allow: JSON.parse(options.get("allowedHtmlTags"))
|
allow: JSON.parse(options.get("allowedHtmlTags"))
|
||||||
},
|
},
|
||||||
// This value must be kept in sync with the language defined in webpack.config.js.
|
removePlugins: getDisabledPlugins(),
|
||||||
language: "en",
|
...await getCkLocale(opts.uiLanguage)
|
||||||
removePlugins: getDisabledPlugins()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up content language.
|
// Set up content language.
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import link from "../../../services/link";
|
||||||
|
|
||||||
|
export async function applyReferenceLinks(container: HTMLDivElement | HTMLElement) {
|
||||||
|
const referenceLinks = container.querySelectorAll<HTMLDivElement>("a.reference-link");
|
||||||
|
for (const referenceLink of referenceLinks) {
|
||||||
|
await link.loadReferenceLinkTitle($(referenceLink));
|
||||||
|
|
||||||
|
// Wrap in a <span> to match the design while in CKEditor.
|
||||||
|
const spanEl = document.createElement("span");
|
||||||
|
spanEl.replaceChildren(...referenceLink.childNodes);
|
||||||
|
referenceLink.replaceChildren(spanEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"@triliumnext/commons": "workspace:*",
|
"@triliumnext/commons": "workspace:*",
|
||||||
"@triliumnext/server": "workspace:*",
|
"@triliumnext/server": "workspace:*",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.6.0",
|
"electron": "38.7.0",
|
||||||
"@electron-forge/cli": "7.10.2",
|
"@electron-forge/cli": "7.10.2",
|
||||||
"@electron-forge/maker-deb": "7.10.2",
|
"@electron-forge/maker-deb": "7.10.2",
|
||||||
"@electron-forge/maker-dmg": "7.10.2",
|
"@electron-forge/maker-dmg": "7.10.2",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@types/yargs": "17.0.34"
|
"@types/yargs": "17.0.35"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx src/main.ts",
|
"dev": "tsx src/main.ts",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"@triliumnext/desktop": "workspace:*",
|
"@triliumnext/desktop": "workspace:*",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.6.0",
|
"electron": "38.7.0",
|
||||||
"fs-extra": "11.3.2"
|
"fs-extra": "11.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-bullseye-slim AS builder
|
FROM node:24.11.1-bullseye-slim AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-bullseye-slim
|
FROM node:24.11.1-bullseye-slim
|
||||||
# Install only runtime dependencies
|
# Install only runtime dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-alpine AS builder
|
FROM node:24.11.1-alpine AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-alpine
|
FROM node:24.11.1-alpine
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
RUN apk add --no-cache su-exec shadow
|
RUN apk add --no-cache su-exec shadow
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-alpine AS builder
|
FROM node:24.11.1-alpine AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-alpine
|
FROM node:24.11.1-alpine
|
||||||
# Create a non-root user with configurable UID/GID
|
# Create a non-root user with configurable UID/GID
|
||||||
ARG USER=trilium
|
ARG USER=trilium
|
||||||
ARG UID=1001
|
ARG UID=1001
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.11.0-bullseye-slim AS builder
|
FROM node:24.11.1-bullseye-slim AS builder
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
# Install native dependencies since we might be building cross-platform.
|
# Install native dependencies since we might be building cross-platform.
|
||||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
|||||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||||
|
|
||||||
FROM node:24.11.0-bullseye-slim
|
FROM node:24.11.1-bullseye-slim
|
||||||
# Create a non-root user with configurable UID/GID
|
# Create a non-root user with configurable UID/GID
|
||||||
ARG USER=trilium
|
ARG USER=trilium
|
||||||
ARG UID=1001
|
ARG UID=1001
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"node-html-parser": "7.0.1"
|
"node-html-parser": "7.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "0.68.0",
|
"@anthropic-ai/sdk": "0.69.0",
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@electron/remote": "2.1.3",
|
"@electron/remote": "2.1.3",
|
||||||
"@preact/preset-vite": "2.10.2",
|
"@preact/preset-vite": "2.10.2",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"debounce": "3.0.0",
|
"debounce": "3.0.0",
|
||||||
"debug": "4.4.3",
|
"debug": "4.4.3",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"electron": "38.6.0",
|
"electron": "38.7.0",
|
||||||
"electron-debug": "4.1.0",
|
"electron-debug": "4.1.0",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
@@ -98,19 +98,19 @@
|
|||||||
"http-proxy-agent": "7.0.2",
|
"http-proxy-agent": "7.0.2",
|
||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "7.0.6",
|
||||||
"i18next": "25.6.2",
|
"i18next": "25.6.2",
|
||||||
"i18next-fs-backend": "2.6.0",
|
"i18next-fs-backend": "2.6.1",
|
||||||
"image-type": "6.0.0",
|
"image-type": "6.0.0",
|
||||||
"ini": "6.0.0",
|
"ini": "6.0.0",
|
||||||
"is-animated": "2.0.2",
|
"is-animated": "2.0.2",
|
||||||
"is-svg": "6.1.0",
|
"is-svg": "6.1.0",
|
||||||
"jimp": "1.6.0",
|
"jimp": "1.6.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.1",
|
||||||
"marked": "16.4.2",
|
"marked": "16.4.2",
|
||||||
"mime-types": "3.0.1",
|
"mime-types": "3.0.1",
|
||||||
"multer": "2.0.2",
|
"multer": "2.0.2",
|
||||||
"normalize-strings": "1.1.1",
|
"normalize-strings": "1.1.1",
|
||||||
"ollama": "0.6.2",
|
"ollama": "0.6.3",
|
||||||
"openai": "6.8.1",
|
"openai": "6.9.0",
|
||||||
"rand-token": "1.0.1",
|
"rand-token": "1.0.1",
|
||||||
"safe-compare": "1.1.4",
|
"safe-compare": "1.1.4",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
|
|||||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
34
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Attributes.html
generated
vendored
@@ -23,6 +23,29 @@
|
|||||||
</ol>
|
</ol>
|
||||||
<p>These attributes play a crucial role in organizing, categorizing, and
|
<p>These attributes play a crucial role in organizing, categorizing, and
|
||||||
enhancing the functionality of notes.</p>
|
enhancing the functionality of notes.</p>
|
||||||
|
<h2>Types of attributes</h2>
|
||||||
|
<p>Conceptually there are two types of attributes (applying to both labels
|
||||||
|
and relations):</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>System attributes</strong>
|
||||||
|
<br>As the name suggest, these attributes have a special meaning since they
|
||||||
|
are interpreted by Trilium. For example the <code>color</code> attribute
|
||||||
|
will change the color of the note as displayed in the <a class="reference-link"
|
||||||
|
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and links, and <code>iconClass</code> will
|
||||||
|
change the icon of a note.</li>
|
||||||
|
<li><strong>User-defined attributes</strong>
|
||||||
|
<br>These are free-form labels or relations that can be used by the user.
|
||||||
|
They can be used purely for categorization purposes (especially if combined
|
||||||
|
with <a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>),
|
||||||
|
or they can be given meaning through the use of <a class="reference-link"
|
||||||
|
href="#root/_help_CdNpE2pqjmI6">Scripting</a>.</li>
|
||||||
|
</ol>
|
||||||
|
<p>In practice, Trilium makes no direct distinction of whether an attribute
|
||||||
|
is a system one or a user-defined one. A label or relation is considered
|
||||||
|
a system attribute if it matches one of the built-in names (e.g. like the
|
||||||
|
aforementioned <code>iconClass</code>). Keep this in mind when creating
|
||||||
|
<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> in
|
||||||
|
order not to accidentally alter a system attribute (unless intended).</p>
|
||||||
<h2>Viewing the list of attributes</h2>
|
<h2>Viewing the list of attributes</h2>
|
||||||
<p>Both the labels and relations for the current note are displayed in the <em>Owned Attributes</em> section
|
<p>Both the labels and relations for the current note are displayed in the <em>Owned Attributes</em> section
|
||||||
of the <a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>,
|
of the <a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>,
|
||||||
@@ -31,13 +54,14 @@
|
|||||||
only be viewed.</p>
|
only be viewed.</p>
|
||||||
<p>In the list of attributes, labels are prefixed with the <code>#</code> character
|
<p>In the list of attributes, labels are prefixed with the <code>#</code> character
|
||||||
whereas relations are prefixed with the <code>~</code> character.</p>
|
whereas relations are prefixed with the <code>~</code> character.</p>
|
||||||
|
<h2>Attribute Definitions and Promoted Attributes</h2>
|
||||||
|
<p><a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> create
|
||||||
|
a form-like editing experience for attributes, which makes it easy to enhancing
|
||||||
|
the organization and management of attributes</p>
|
||||||
<h2>Multiplicity</h2>
|
<h2>Multiplicity</h2>
|
||||||
<p>Attributes in Trilium can be "multi-valued", meaning multiple attributes
|
<p>Attributes in Trilium can be "multi-valued", meaning multiple attributes
|
||||||
with the same name can co-exist.</p>
|
with the same name can co-exist. This can be combined with <a class="reference-link"
|
||||||
<h2>Attribute Definitions and Promoted Attributes</h2>
|
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> to easily add them.</p>
|
||||||
<p>Special labels create "label/attribute" definitions, enhancing the organization
|
|
||||||
and management of attributes. For more details, see <a class="reference-link"
|
|
||||||
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>
|
|
||||||
<h2>Attribute Inheritance</h2>
|
<h2>Attribute Inheritance</h2>
|
||||||
<p>Trilium supports attribute inheritance, allowing child notes to inherit
|
<p>Trilium supports attribute inheritance, allowing child notes to inherit
|
||||||
attributes from their parents. For more information, see <a class="reference-link"
|
attributes from their parents. For more information, see <a class="reference-link"
|
||||||
|
|||||||
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Attributes/1_Promoted Attributes_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Attributes/2_Promoted Attributes_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Attributes/3_Promoted Attributes_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
@@ -1,31 +1,116 @@
|
|||||||
|
<figure class="image image_resized" style="width:61.4%;">
|
||||||
|
<img style="aspect-ratio:938/368;" src="Promoted Attributes_image.png"
|
||||||
|
width="938" height="368">
|
||||||
|
</figure>
|
||||||
<p>Promoted attributes are <a href="#root/_help_zEY4DaJG4YT5">attributes</a> which
|
<p>Promoted attributes are <a href="#root/_help_zEY4DaJG4YT5">attributes</a> which
|
||||||
are considered important and thus are "promoted" onto the main note UI.
|
are displayed prominently in the UI which allow them to be easily viewed
|
||||||
See example below:</p>
|
and edited.</p>
|
||||||
<p>
|
<p>One way of seeing promoted attributes is as a kind of form with several
|
||||||
<img src="Promoted Attributes_promot.png">
|
fields. Each field is just regular attribute, the only difference is that
|
||||||
</p>
|
they appear on the note itself.</p>
|
||||||
<p>You can see the note having kind of form with several fields. Each of
|
|
||||||
these is just regular attribute, the only difference is that they appear
|
|
||||||
on the note itself.</p>
|
|
||||||
<p>Attributes can be pretty useful since they allow for querying and script
|
<p>Attributes can be pretty useful since they allow for querying and script
|
||||||
automation etc. but they are also inconveniently hidden. This allows you
|
automation etc. but they are also inconveniently hidden. This allows you
|
||||||
to select few of the important ones and push them to the front of the user.</p>
|
to select few of the important ones and push them to the front of the user.</p>
|
||||||
<p>Now, how do we make attribute to appear on the UI?</p>
|
|
||||||
<h2>Attribute definition</h2>
|
<h2>Attribute definition</h2>
|
||||||
<p>Attribute is always name-value pair where both name and value are strings.</p>
|
<p>In order to have promoted attributes, there needs to be a way to define
|
||||||
<p><em>Attribute definition</em> specifies how should this value be interpreted
|
them.</p>
|
||||||
- is it just string, or is it a date? Should we allow multiple values or
|
<figure class="image image-style-align-right image_resized" style="width:38.82%;">
|
||||||
note? And importantly, should we <em>promote</em> the attribute or not?</p>
|
<img style="aspect-ratio:492/346;" src="1_Promoted Attributes_image.png"
|
||||||
<p>
|
width="492" height="346">
|
||||||
<img src="Promoted Attributes_image.png">
|
</figure>
|
||||||
</p>
|
<p>Technically, attributes are only name-value pairs where both name and
|
||||||
<p>You can notice tag attribute definition. These "definition" attributes
|
value are strings.</p>
|
||||||
define how the "value" attributes should behave.</p>
|
<p>The <em>Attribute definition</em> specifies how should this value be interpreted:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Is it just string, or is it a date?</li>
|
||||||
|
<li>Should we allow multiple values or note?</li>
|
||||||
|
<li>Should we <em>promote</em> the attribute or not?</li>
|
||||||
|
</ul>
|
||||||
|
<h2>Creating a new promoted attribute definition</h2>
|
||||||
|
<p>To create a new promoted attribute:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Go to a note.</li>
|
||||||
|
<li>Go to <em>Owned Attributes</em> in the <a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>.</li>
|
||||||
|
<li>Press the + button.</li>
|
||||||
|
<li>Select either <em>Add new label definition</em> or <em>Add new relation definition</em>.</li>
|
||||||
|
<li>Select the name which will be name of the label or relation that will
|
||||||
|
be created when the promoted attribute is edited.</li>
|
||||||
|
<li>Ensure <em>Promoted</em> is checked in order to display it at the top of
|
||||||
|
notes.</li>
|
||||||
|
<li>Optionally, choose an <em>Alias</em> which will be displayed next to the
|
||||||
|
promoted attribute instead of the attribute name. Generally it's best to
|
||||||
|
choose a “user-friendly” name since it can contain spaces and other characters
|
||||||
|
which are not supported as attribute names.</li>
|
||||||
|
<li>Check <em>Inheritable</em> to apply it to this note and all its descendants.
|
||||||
|
To keep it only for the current note, un-check it.</li>
|
||||||
|
<li>Press “Save & Close” to apply the changes.</li>
|
||||||
|
</ol>
|
||||||
|
<h2>How attribute definitions actually work</h2>
|
||||||
|
<p>When a new promoted attribute definition is created, it creates a corresponding
|
||||||
|
label prefixed with either <code>label</code> or <code>relation</code>, depending
|
||||||
|
on the definition type:</p><pre><code class="language-text-x-trilium-auto">#label:myColor(inheritable)="promoted,alias=Color,multi,color"</code></pre>
|
||||||
|
<p>The only purpose of the attribute definition is to set up a template.
|
||||||
|
If the attribute was marked as promoted, then it's also displayed to the
|
||||||
|
user for easy editing.</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<figure class="image">
|
||||||
|
<img style="aspect-ratio:495/157;" src="2_Promoted Attributes_image.png"
|
||||||
|
width="495" height="157">
|
||||||
|
</figure>
|
||||||
|
</td>
|
||||||
|
<td>Notice how the promoted attribute definition only creates a “Due date”
|
||||||
|
box above the text content.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<figure class="image">
|
||||||
|
<img style="aspect-ratio:663/160;" src="3_Promoted Attributes_image.png"
|
||||||
|
width="663" height="160">
|
||||||
|
</figure>
|
||||||
|
</td>
|
||||||
|
<td>Once a value is set by the user, a new label (or relation, depending on
|
||||||
|
the type) is created. The name of the attribute matches one set when creating
|
||||||
|
the promoted attribute.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<p>So there's one attribute for value and one for definition. But notice
|
<p>So there's one attribute for value and one for definition. But notice
|
||||||
how definition attribute is <a href="#root/_help_bwZpz2ajCEwO">Inheritable</a>,
|
how an definition attribute can be made <a href="#root/_help_bwZpz2ajCEwO">Inheritable</a>,
|
||||||
meaning that it's also applied to all descendant note. So in a way, this
|
meaning that it's also applied to all descendant notes. In this case, the
|
||||||
definition is used for the whole subtree while "value" attributes are applied
|
definition used for the whole sub-tree while "value" attributes are for
|
||||||
only for this note.</p>
|
each not individually.</p>
|
||||||
|
<h2>Using system attributes</h2>
|
||||||
|
<p>It's possible to create promoted attributes out of system attributes,
|
||||||
|
to be able to easily alter them.</p>
|
||||||
|
<p>Here are a few practical examples:</p>
|
||||||
|
<ul>
|
||||||
|
<li><a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> already
|
||||||
|
make use of this practice, for example:
|
||||||
|
<ul>
|
||||||
|
<li>Calendars add “Start Date”, “End Date”, “Start Time” and “End Time” as
|
||||||
|
promoted attributes. These map to system attributes such as <code>startDate</code> which
|
||||||
|
are then interpreted by the calendar view.</li>
|
||||||
|
<li><a class="reference-link" href="#root/_help_zP3PMqaG71Ct">Presentation</a> adds
|
||||||
|
a “Background” promoted attribute for each of the slide to easily be able
|
||||||
|
to customize.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>The Trilium documentation (which is edited in Trilium) uses a promoted
|
||||||
|
attribute to be able to easily edit the <code>#shareAlias</code> (see
|
||||||
|
<a
|
||||||
|
class="reference-link" href="#root/_help_R9pX4DGra2Vt">Sharing</a>) in order to form clean URLs.</li>
|
||||||
|
<li>If you always edit a particular system attribute such as <code>#color</code>,
|
||||||
|
simply create a promoted attribute for it to make it easier.</li>
|
||||||
|
</ul>
|
||||||
<h3>Inverse relation</h3>
|
<h3>Inverse relation</h3>
|
||||||
<p>Some relations always occur in pairs - my favorite example is on the family.
|
<p>Some relations always occur in pairs - my favorite example is on the family.
|
||||||
If you have a note representing husband and note representing wife, then
|
If you have a note representing husband and note representing wife, then
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 44 KiB |
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/1_Kanban Board_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/2_Kanban Board_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
6
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
@@ -112,6 +112,12 @@
|
|||||||
<td>When present (regardless of value), it will show the number of the week
|
<td>When present (regardless of value), it will show the number of the week
|
||||||
on the calendar.</td>
|
on the calendar.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>#calendar:initialDate</code>
|
||||||
|
</td>
|
||||||
|
<td>Change the date the calendar opens on. When not present, the calendar
|
||||||
|
opens on the current date.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>#calendar:view</code>
|
<td><code>#calendar:view</code>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
24
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Geo Map.html
generated
vendored
@@ -1,8 +1,9 @@
|
|||||||
<aside class="admonition important">
|
<aside class="admonition important">
|
||||||
<p>Starting with Trilium v0.97.0, the geo map has been converted from a standalone
|
<p><a class="reference-link" href="#root/_help_zEY4DaJG4YT5">Attributes</a><a class="reference-link"
|
||||||
<a
|
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a><a class="reference-link"
|
||||||
href="#root/_help_KSZ04uQ2D1St">note type</a>to a type of view for the <a class="reference-link"
|
href="#root/_help_zEY4DaJG4YT5">Attributes</a>Starting with Trilium v0.97.0,
|
||||||
href="#root/_help_0ESUbbAxVnoK">Note List</a>. </p>
|
the geo map has been converted from a standalone <a href="#root/_help_KSZ04uQ2D1St">note type</a> to
|
||||||
|
a type of view for the <a class="reference-link" href="#root/_help_0ESUbbAxVnoK">Note List</a>. </p>
|
||||||
</aside>
|
</aside>
|
||||||
<figure class="image image-style-align-center">
|
<figure class="image image-style-align-center">
|
||||||
<img style="aspect-ratio:892/675;" src="9_Geo Map_image.png"
|
<img style="aspect-ratio:892/675;" src="9_Geo Map_image.png"
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
<td>To create a marker, first navigate to the desired point on the map. Then
|
<td>To create a marker, first navigate to the desired point on the map. Then
|
||||||
press the
|
press the
|
||||||
<img src="10_Geo Map_image.png">button in the <a href="#root/_help_XpOYSgsLkTJy">Floating buttons</a> (top-right)
|
<img src="10_Geo Map_image.png">button in the <a href="#root/_help_XpOYSgsLkTJy">Floating buttons</a> (top-right)
|
||||||
area.
|
area.
|
||||||
<br>
|
<br>
|
||||||
<br>If the button is not visible, make sure the button section is visible
|
<br>If the button is not visible, make sure the button section is visible
|
||||||
by pressing the chevron button (
|
by pressing the chevron button (
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
width="1730" height="416">
|
width="1730" height="416">
|
||||||
</td>
|
</td>
|
||||||
<td>Once pressed, the map will enter in the insert mode, as illustrated by
|
<td>Once pressed, the map will enter in the insert mode, as illustrated by
|
||||||
the notification.
|
the notification.
|
||||||
<br>
|
<br>
|
||||||
<br>Simply click the point on the map where to place the marker, or the Escape
|
<br>Simply click the point on the map where to place the marker, or the Escape
|
||||||
key to cancel.</td>
|
key to cancel.</td>
|
||||||
@@ -112,7 +113,8 @@
|
|||||||
<li>Right click anywhere on the map, where to place the newly created marker
|
<li>Right click anywhere on the map, where to place the newly created marker
|
||||||
(and corresponding note).</li>
|
(and corresponding note).</li>
|
||||||
<li>Select <em>Add a marker at this location</em>.</li>
|
<li>Select <em>Add a marker at this location</em>.</li>
|
||||||
<li>Enter the name of the newly created note.</li>
|
<li>Enter the name of the ne<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>wly
|
||||||
|
created note.</li>
|
||||||
<li>The map should be updated with the new marker.</li>
|
<li>The map should be updated with the new marker.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<h3>Adding an existing note on note from the note tree</h3>
|
<h3>Adding an existing note on note from the note tree</h3>
|
||||||
@@ -221,10 +223,10 @@ width="1288" height="278">
|
|||||||
</figure>
|
</figure>
|
||||||
</td>
|
</td>
|
||||||
<td>Go to Google Maps on the web and look for a desired location, right click
|
<td>Go to Google Maps on the web and look for a desired location, right click
|
||||||
on it and a context menu will show up.
|
on it and a context menu will show up.
|
||||||
<br>
|
<br>
|
||||||
<br>Simply click on the first item displaying the coordinates and they will
|
<br>Simply click on the first item displaying the coordinates and they will
|
||||||
be copied to clipboard.
|
be copied to clipboard.
|
||||||
<br>
|
<br>
|
||||||
<br>Then paste the value inside the text box into the <code>#geolocation</code> attribute
|
<br>Then paste the value inside the text box into the <code>#geolocation</code> attribute
|
||||||
of a child note of the map (don't forget to surround the value with a <code>"</code> character).</td>
|
of a child note of the map (don't forget to surround the value with a <code>"</code> character).</td>
|
||||||
@@ -282,7 +284,7 @@ width="1288" height="278">
|
|||||||
width="696" height="480">
|
width="696" height="480">
|
||||||
</td>
|
</td>
|
||||||
<td>The address will be visible in the top-left of the screen, in the place
|
<td>The address will be visible in the top-left of the screen, in the place
|
||||||
of the search bar.
|
of the search bar.
|
||||||
<br>
|
<br>
|
||||||
<br>Select the coordinates and copy them into the clipboard.</td>
|
<br>Select the coordinates and copy them into the clipboard.</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -339,7 +341,7 @@ width="1288" height="278">
|
|||||||
width="620" height="530">
|
width="620" height="530">
|
||||||
</figure>
|
</figure>
|
||||||
</td>
|
</td>
|
||||||
<td>When going back to the map, the track should now be visible.
|
<td>When going back to the map, the track should now be visible.
|
||||||
<br>
|
<br>
|
||||||
<br>The start and end points of the track are indicated by the two blue markers.</td>
|
<br>The start and end points of the track are indicated by the two blue markers.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
102
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Kanban Board.html
generated
vendored
@@ -1,5 +1,5 @@
|
|||||||
<figure class="image">
|
<figure class="image">
|
||||||
<img style="aspect-ratio:918/248;" src="Kanban Board_image.png"
|
<img style="aspect-ratio:918/248;" src="2_Kanban Board_image.png"
|
||||||
width="918" height="248">
|
width="918" height="248">
|
||||||
</figure>
|
</figure>
|
||||||
<p>The Board view presents sub-notes in columns for a Kanban-like experience.
|
<p>The Board view presents sub-notes in columns for a Kanban-like experience.
|
||||||
@@ -70,7 +70,22 @@
|
|||||||
<li>If there are many notes within the column, move the mouse over the column
|
<li>If there are many notes within the column, move the mouse over the column
|
||||||
and use the mouse wheel to scroll.</li>
|
and use the mouse wheel to scroll.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Keyboard interaction</h2>
|
<h3>Working with the note tree</h3>
|
||||||
|
<p>It's also possible to add items on the board using the <a class="reference-link"
|
||||||
|
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</p>
|
||||||
|
<ol>
|
||||||
|
<li>Select the desired note in the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
|
||||||
|
<li>Hold the mouse on the note and drag it to the to the desired column.</li>
|
||||||
|
</ol>
|
||||||
|
<p>This works for:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Notes that are not children of the board, case in which a <a href="#root/_help_IakOLONlIfGI">clone</a> will
|
||||||
|
be created.</li>
|
||||||
|
<li>Notes that are children of the board, but not yet assigned on the board.</li>
|
||||||
|
<li>Notes that are children of the board, case in which they will be moved
|
||||||
|
to the new column.</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Keyboard interaction</h3>
|
||||||
<p>The board view has mild support for keyboard-based navigation:</p>
|
<p>The board view has mild support for keyboard-based navigation:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Use <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> to navigate between
|
<li>Use <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> to navigate between
|
||||||
@@ -82,16 +97,81 @@
|
|||||||
<li>To dismiss a rename of a note or a column, press <kbd>Escape</kbd>.</li>
|
<li>To dismiss a rename of a note or a column, press <kbd>Escape</kbd>.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Configuration</h2>
|
<h2>Configuration</h2>
|
||||||
<h3>Grouping by another attribute</h3>
|
<h3>Displaying custom attributes</h3>
|
||||||
|
<figure class="image image-style-align-center">
|
||||||
|
<img style="aspect-ratio:531/485;" src="Kanban Board_image.png"
|
||||||
|
width="531" height="485">
|
||||||
|
</figure>
|
||||||
|
<p>Note attributes can be displayed on the board to enhance it with custom
|
||||||
|
information such as adding a Due date for your tasks.</p>
|
||||||
|
<p>This feature works exclusively via attribute definitions (<a class="reference-link"
|
||||||
|
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>). The easiest way to
|
||||||
|
add these is:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Go to board note.</li>
|
||||||
|
<li>In the ribbon select <em>Owned Attributes</em> → plus button → <em>Add new label/relation definition</em>.</li>
|
||||||
|
<li>Configure the attribute as desired.</li>
|
||||||
|
<li>Check <em>Inheritable</em> to make it applicable to child notes automatically.</li>
|
||||||
|
</ol>
|
||||||
|
<p>After creating the attribute, click on a note and fill in the promoted
|
||||||
|
attributes which should then reflect inside the board.</p>
|
||||||
|
<p>Of note:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Both promoted and non-promoted attribute definitions are supported. The
|
||||||
|
only difference is that non-promoted attributes don't have an “Alias” for
|
||||||
|
assigning a custom name.</li>
|
||||||
|
<li>Both “Single value” and “Multi value” attributes are supported. In case
|
||||||
|
of multi-value, a badge is displayed for every instance of the attribute.</li>
|
||||||
|
<li>All label types are supported, including dates, booleans and URLs.</li>
|
||||||
|
<li>Relation attributes are also supported as well, showing a link with the
|
||||||
|
target note title and icon.</li>
|
||||||
|
<li>Currently, it's not possible to adjust which promoted attributes are displayed,
|
||||||
|
since all promoted attributes will be displayed (except the <code>board:groupBy</code> one).
|
||||||
|
There are plans to improve upon this being able to hide promoted attributes
|
||||||
|
individually.</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Grouping by another label</h3>
|
||||||
<p>By default, the label used to group the notes is <code>#status</code>.
|
<p>By default, the label used to group the notes is <code>#status</code>.
|
||||||
It is possible to use a different label if needed by defining a label named <code>#board:groupBy</code> with
|
It is possible to use a different label if needed by defining a label named <code>#board:groupBy</code> with
|
||||||
the value being the attribute to use (without <code>#</code> attribute prefix).</p>
|
the value being the attribute to use (with or without <code>#</code> attribute
|
||||||
<aside
|
prefix).</p>
|
||||||
class="admonition note">
|
<h3>Grouping by relations</h3>
|
||||||
<p>It's currently not possible to set a relation as the grouping criteria.
|
<figure class="image image-style-align-right">
|
||||||
There are plans to add support for it.</p>
|
<img style="aspect-ratio:535/245;" src="1_Kanban Board_image.png"
|
||||||
</aside>
|
width="535" height="245">
|
||||||
<h2>Limitations</h2>
|
</figure>
|
||||||
|
<p>A more advanced use-case is grouping by <a href="#root/_help_Cq5X6iKQop6R">Relations</a>.</p>
|
||||||
|
<p>During this mode:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>It is not possible yet to use group by a relation, only by label.</li>
|
<li>The columns represent the <em>target notes</em> of a relation.</li>
|
||||||
|
<li>When creating a new column, a note is selected instead of a column name.</li>
|
||||||
|
<li>The column icon will match the target note.</li>
|
||||||
|
<li>Moving notes between columns will change its relation.</li>
|
||||||
|
<li>Renaming an existing column will change the target note of all the notes
|
||||||
|
in that column.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p>Using relations instead of labels has some benefits:</p>
|
||||||
|
<ul>
|
||||||
|
<li>The status/grouping of the notes is visible outside the Kanban board,
|
||||||
|
for example on the <a class="reference-link" href="#root/_help_bdUJEHsAPYQR">Note Map</a>.</li>
|
||||||
|
<li>Columns can have icons.</li>
|
||||||
|
<li>Renaming columns is less intensive since it simply involves changing the
|
||||||
|
note title of the target note instead of having to do a bulk rename.</li>
|
||||||
|
</ul>
|
||||||
|
<p>To do so:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<p>First, create a Kanban board from scratch and not a template:</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Assign <code>#viewType=board #hidePromotedAttributes</code> to emulate the
|
||||||
|
default template.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Set <code>#board:groupBy</code> to the name of a relation to group by, <strong>including the</strong> <code>**~**</code> <strong>prefix</strong> (e.g. <code>~status</code>).</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Optionally, use <a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a> for
|
||||||
|
easy status change within the note:</p><pre><code class="language-text-x-trilium-auto">#relation:status(inheritable)="promoted,alias=Status,single"</code></pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Kanban Board_image.png
generated
vendored
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 47 KiB |
@@ -135,7 +135,7 @@ docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-
|
|||||||
(default: <code>/home/node/trilium-data</code>)</li>
|
(default: <code>/home/node/trilium-data</code>)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>For a complete list of configuration environment variables (network settings,
|
<p>For a complete list of configuration environment variables (network settings,
|
||||||
authentication, sync, etc.), see <a class="reference-link" href="#root/_help_dmi3wz9muS2O">Configuration (config.ini or environment variables)</a>.</p>
|
authentication, sync, etc.), see <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a>.</p>
|
||||||
<h3>Volume Permissions</h3>
|
<h3>Volume Permissions</h3>
|
||||||
<p>If you encounter permission issues with the data volume, ensure that:</p>
|
<p>If you encounter permission issues with the data volume, ensure that:</p>
|
||||||
<ol>
|
<ol>
|
||||||
|
|||||||
48
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Traefik.html
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<p>Configure Traefik proxy and HTTPS. See <a href="https://github.com/TriliumNext/Trilium/issues/7768#issuecomment-3539165814">#7768</a> for
|
||||||
|
reference</p>
|
||||||
|
<h3>Build the docker-compose file</h3>
|
||||||
|
<p>Setting up Traefik as reverse proxy requires setting the following labels:</p><pre><code class="language-text-x-yaml"> labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.trilium.entrypoints=https
|
||||||
|
- traefik.http.routers.trilium.rule=Host(`trilium.mydomain.tld`)
|
||||||
|
- traefik.http.routers.trilium.tls=true
|
||||||
|
- traefik.http.routers.trilium.service=trilium
|
||||||
|
- traefik.http.services.trilium.loadbalancer.server.port=8080
|
||||||
|
# scheme must be HTTP instead of the usual HTTPS because Trilium listens on HTTP internally
|
||||||
|
- traefik.http.services.trilium.loadbalancer.server.scheme=http
|
||||||
|
- traefik.docker.network=proxy
|
||||||
|
# forward HTTP to HTTPS
|
||||||
|
- traefik.http.routers.trilium.middlewares=trilium-headers@docker
|
||||||
|
- traefik.http.middlewares.trilium-headers.headers.customrequestheaders.X-Forwarded-Proto=https</code></pre>
|
||||||
|
<h3>Setup needed environment variables</h3>
|
||||||
|
<p>After setting up a reverse proxy, make sure to configure the <a class="reference-link"
|
||||||
|
href="Trusted%20proxy.md">[missing note]</a>.</p>
|
||||||
|
<h3>Example <code>docker-compose.yaml</code></h3><pre><code class="language-text-x-yaml">services:
|
||||||
|
trilium:
|
||||||
|
image: triliumnext/trilium
|
||||||
|
container_name: trilium
|
||||||
|
networks:
|
||||||
|
- traefik-proxy
|
||||||
|
environment:
|
||||||
|
- TRILIUM_NETWORK_TRUSTEDREVERSEPROXY=my-traefik-host-ip # e.g., 172.18.0.0/16
|
||||||
|
volumes:
|
||||||
|
- /path/to/data:/home/node/trilium-data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.trilium.entrypoints=https
|
||||||
|
- traefik.http.routers.trilium.rule=Host(`trilium.mydomain.tld`)
|
||||||
|
- traefik.http.routers.trilium.tls=true
|
||||||
|
- traefik.http.routers.trilium.service=trilium
|
||||||
|
- traefik.http.services.trilium.loadbalancer.server.port=8080
|
||||||
|
# scheme must be HTTP instead of the usual HTTPS because of how trilium works
|
||||||
|
- traefik.http.services.trilium.loadbalancer.server.scheme=http
|
||||||
|
- traefik.docker.network=traefik-proxy
|
||||||
|
# Tell Trilium the original request was HTTPS
|
||||||
|
- traefik.http.routers.trilium.middlewares=trilium-headers@docker
|
||||||
|
- traefik.http.middlewares.trilium-headers.headers.customrequestheaders.X-Forwarded-Proto=https
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-proxy:
|
||||||
|
external: true</code></pre>
|
||||||
@@ -258,7 +258,9 @@
|
|||||||
"jump-to-note-title": "跳转至...",
|
"jump-to-note-title": "跳转至...",
|
||||||
"llm-chat-title": "与笔记聊天",
|
"llm-chat-title": "与笔记聊天",
|
||||||
"ai-llm-title": "AI/LLM",
|
"ai-llm-title": "AI/LLM",
|
||||||
"inbox-title": "收件箱"
|
"inbox-title": "收件箱",
|
||||||
|
"command-palette": "打开命令面板",
|
||||||
|
"zen-mode": "禅模式"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"new-note": "新建笔记",
|
"new-note": "新建笔记",
|
||||||
|
|||||||
@@ -43,6 +43,26 @@
|
|||||||
},
|
},
|
||||||
"hidden-subtree": {
|
"hidden-subtree": {
|
||||||
"zen-mode": "젠 모드",
|
"zen-mode": "젠 모드",
|
||||||
"open-today-journal-note-title": "오늘의 일지 기록 열기"
|
"open-today-journal-note-title": "오늘의 일지 기록 열기",
|
||||||
|
"quick-search-title": "빠른 검색",
|
||||||
|
"protected-session-title": "보호된 세션",
|
||||||
|
"sync-status-title": "동기화 상태",
|
||||||
|
"settings-title": "설정",
|
||||||
|
"llm-chat-title": "기록과 대화하기",
|
||||||
|
"options-title": "옵션",
|
||||||
|
"appearance-title": "모양",
|
||||||
|
"shortcuts-title": "바로가기",
|
||||||
|
"text-notes": "텍스트 노트",
|
||||||
|
"code-notes-title": "코드 노트",
|
||||||
|
"images-title": "그림",
|
||||||
|
"spellcheck-title": "맞춤법 검사",
|
||||||
|
"password-title": "암호",
|
||||||
|
"multi-factor-authentication-title": "다중 인증",
|
||||||
|
"etapi-title": "ETAPI",
|
||||||
|
"backup-title": "백업",
|
||||||
|
"sync-title": "동기화",
|
||||||
|
"ai-llm-title": "AI/LLM",
|
||||||
|
"other": "기타",
|
||||||
|
"advanced-title": "고급"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,9 @@
|
|||||||
"multi-factor-authentication-title": "Autentificare multi-factor",
|
"multi-factor-authentication-title": "Autentificare multi-factor",
|
||||||
"ai-llm-title": "AI/LLM",
|
"ai-llm-title": "AI/LLM",
|
||||||
"localization": "Limbă și regiune",
|
"localization": "Limbă și regiune",
|
||||||
"inbox-title": "Inbox"
|
"inbox-title": "Inbox",
|
||||||
|
"command-palette": "Deschide paleta de comenzi",
|
||||||
|
"zen-mode": "Mod zen"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"new-note": "Notiță nouă",
|
"new-note": "Notiță nouă",
|
||||||
@@ -274,7 +276,8 @@
|
|||||||
"export_filter": "Document PDF (*.pdf)",
|
"export_filter": "Document PDF (*.pdf)",
|
||||||
"unable-to-export-message": "Notița curentă nu a putut fi exportată ca PDF.",
|
"unable-to-export-message": "Notița curentă nu a putut fi exportată ca PDF.",
|
||||||
"unable-to-export-title": "Nu s-a putut exporta ca PDF",
|
"unable-to-export-title": "Nu s-a putut exporta ca PDF",
|
||||||
"unable-to-save-message": "Nu s-a putut scrie fișierul selectat. Încercați din nou sau selectați altă destinație."
|
"unable-to-save-message": "Nu s-a putut scrie fișierul selectat. Încercați din nou sau selectați altă destinație.",
|
||||||
|
"unable-to-print": "Nu s-a putut imprima notița"
|
||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"bookmarks": "Semne de carte",
|
"bookmarks": "Semne de carte",
|
||||||
@@ -427,7 +430,8 @@
|
|||||||
"presentation": "Prezentare",
|
"presentation": "Prezentare",
|
||||||
"presentation_slide": "Slide de prezentare",
|
"presentation_slide": "Slide de prezentare",
|
||||||
"presentation_slide_first": "Primul slide",
|
"presentation_slide_first": "Primul slide",
|
||||||
"presentation_slide_second": "Al doilea slide"
|
"presentation_slide_second": "Al doilea slide",
|
||||||
|
"background": "Fundal"
|
||||||
},
|
},
|
||||||
"sql_init": {
|
"sql_init": {
|
||||||
"db_not_initialized_desktop": "Baza de date nu este inițializată, urmați instrucțiunile de pe ecran.",
|
"db_not_initialized_desktop": "Baza de date nu este inițializată, urmați instrucțiunile de pe ecran.",
|
||||||
|
|||||||
@@ -355,7 +355,9 @@
|
|||||||
"visible-launchers-title": "可見啟動器",
|
"visible-launchers-title": "可見啟動器",
|
||||||
"user-guide": "用戶說明",
|
"user-guide": "用戶說明",
|
||||||
"localization": "語言和區域",
|
"localization": "語言和區域",
|
||||||
"inbox-title": "收件匣"
|
"inbox-title": "收件匣",
|
||||||
|
"command-palette": "打開命令面板",
|
||||||
|
"zen-mode": "禪模式"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"new-note": "新增筆記",
|
"new-note": "新增筆記",
|
||||||
|
|||||||
@@ -91,9 +91,12 @@ function validateUtcDateTime(str: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
LOCAL_DATETIME_FORMAT,
|
||||||
|
UTC_DATETIME_FORMAT,
|
||||||
utcNowDateTime,
|
utcNowDateTime,
|
||||||
localNowDateTime,
|
localNowDateTime,
|
||||||
localNowDate,
|
localNowDate,
|
||||||
|
|
||||||
utcDateStr,
|
utcDateStr,
|
||||||
utcDateTimeStr,
|
utcDateTimeStr,
|
||||||
parseDateTime,
|
parseDateTime,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
import sax from "sax";
|
import sax from "sax";
|
||||||
import stream from "stream";
|
import stream from "stream";
|
||||||
import { Throttle } from "stream-throttle";
|
import { Throttle } from "stream-throttle";
|
||||||
import log from "../log.js";
|
import log from "../log.js";
|
||||||
import { md5, escapeHtml, fromBase64 } from "../utils.js";
|
import { md5, escapeHtml, fromBase64 } from "../utils.js";
|
||||||
|
import date_utils from "../date_utils.js";
|
||||||
import sql from "../sql.js";
|
import sql from "../sql.js";
|
||||||
import noteService from "../notes.js";
|
import noteService from "../notes.js";
|
||||||
import imageService from "../image.js";
|
import imageService from "../image.js";
|
||||||
@@ -235,6 +237,8 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
|
|||||||
|
|
||||||
function updateDates(note: BNote, utcDateCreated?: string, utcDateModified?: string) {
|
function updateDates(note: BNote, utcDateCreated?: string, utcDateModified?: string) {
|
||||||
// it's difficult to force custom dateCreated and dateModified to Note entity, so we do it post-creation with SQL
|
// it's difficult to force custom dateCreated and dateModified to Note entity, so we do it post-creation with SQL
|
||||||
|
const dateCreated = formatDateTimeToLocalDbFormat(utcDateCreated, false);
|
||||||
|
const dateModified = formatDateTimeToLocalDbFormat(utcDateModified, false);
|
||||||
sql.execute(
|
sql.execute(
|
||||||
`
|
`
|
||||||
UPDATE notes
|
UPDATE notes
|
||||||
@@ -243,7 +247,7 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
|
|||||||
dateModified = ?,
|
dateModified = ?,
|
||||||
utcDateModified = ?
|
utcDateModified = ?
|
||||||
WHERE noteId = ?`,
|
WHERE noteId = ?`,
|
||||||
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, note.noteId]
|
[dateCreated, utcDateCreated, dateModified, utcDateModified, note.noteId]
|
||||||
);
|
);
|
||||||
|
|
||||||
sql.execute(
|
sql.execute(
|
||||||
@@ -407,4 +411,21 @@ function importEnex(taskContext: TaskContext<"importNotes">, file: File, parentN
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTimeToLocalDbFormat(
|
||||||
|
utcDateFromEnex: Date | string | null | undefined,
|
||||||
|
keepUtc: boolean
|
||||||
|
): string | undefined {
|
||||||
|
if (!utcDateFromEnex) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDate = dayjs(utcDateFromEnex);
|
||||||
|
|
||||||
|
if (!parsedDate.isValid()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (keepUtc ? parsedDate.utc() : parsedDate).format(date_utils.LOCAL_DATETIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
export default { importEnex };
|
export default { importEnex };
|
||||||
|
|||||||
@@ -113,7 +113,16 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
const normalizedFlatText = normalizeSearchText(flatText);
|
const normalizedFlatText = normalizeSearchText(flatText);
|
||||||
|
|
||||||
// Check if =phrase appears in flatText (indicates attribute value match)
|
// Check if =phrase appears in flatText (indicates attribute value match)
|
||||||
|
// For single words, use word-boundary matching to avoid substring matches
|
||||||
|
if (!normalizedPhrase.includes(' ')) {
|
||||||
|
// Single word: look for =word with word boundaries
|
||||||
|
// Split by = to get attribute values, then check each value for exact word match
|
||||||
|
const parts = normalizedFlatText.split('=');
|
||||||
|
matches = parts.slice(1).some(part => this.exactWordMatch(normalizedPhrase, part));
|
||||||
|
} else {
|
||||||
|
// Multi-word phrase: check for substring match
|
||||||
matches = normalizedFlatText.includes(`=${normalizedPhrase}`);
|
matches = normalizedFlatText.includes(`=${normalizedPhrase}`);
|
||||||
|
}
|
||||||
|
|
||||||
if ((this.operator === "=" && matches) || (this.operator === "!=" && !matches)) {
|
if ((this.operator === "=" && matches) || (this.operator === "!=" && !matches)) {
|
||||||
resultNoteSet.add(noteFromBecca);
|
resultNoteSet.add(noteFromBecca);
|
||||||
@@ -124,6 +133,17 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
return resultNoteSet;
|
return resultNoteSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to check if a single word appears as an exact match in text
|
||||||
|
* @param wordToFind - The word to search for (should be normalized)
|
||||||
|
* @param text - The text to search in (should be normalized)
|
||||||
|
* @returns true if the word is found as an exact match (not substring)
|
||||||
|
*/
|
||||||
|
private exactWordMatch(wordToFind: string, text: string): boolean {
|
||||||
|
const words = text.split(/\s+/);
|
||||||
|
return words.some(word => word === wordToFind);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if content contains the exact word (with word boundaries) or exact phrase
|
* Checks if content contains the exact word (with word boundaries) or exact phrase
|
||||||
* This is case-insensitive since content and token are already normalized
|
* This is case-insensitive since content and token are already normalized
|
||||||
@@ -139,9 +159,8 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
return normalizedContent.includes(normalizedToken);
|
return normalizedContent.includes(normalizedToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For single words, split content into words and check for exact match
|
// For single words, use exact word matching to avoid substring matches
|
||||||
const words = normalizedContent.split(/\s+/);
|
return this.exactWordMatch(normalizedToken, normalizedContent);
|
||||||
return words.some(word => word === normalizedToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,7 +174,14 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
// Join tokens with single space to form the phrase
|
// Join tokens with single space to form the phrase
|
||||||
const phrase = normalizedTokens.join(" ");
|
const phrase = normalizedTokens.join(" ");
|
||||||
|
|
||||||
// Check if the phrase appears as a substring (consecutive words)
|
// For single-word phrases, use word-boundary matching to avoid substring matches
|
||||||
|
// e.g., "asd" should not match "asdfasdf"
|
||||||
|
if (!phrase.includes(' ')) {
|
||||||
|
// Single word: use exact word matching to avoid substring matches
|
||||||
|
return this.exactWordMatch(phrase, normalizedContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multi-word phrases, check if the phrase appears as consecutive words
|
||||||
if (normalizedContent.includes(phrase)) {
|
if (normalizedContent.includes(phrase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import BBranch from "../becca/entities/bbranch.js";
|
|||||||
import BNote from "../becca/entities/bnote.js";
|
import BNote from "../becca/entities/bnote.js";
|
||||||
import tree from "./tree.js";
|
import tree from "./tree.js";
|
||||||
import cls from "./cls.js";
|
import cls from "./cls.js";
|
||||||
|
import { buildNote } from "../test/becca_easy_mocking.js";
|
||||||
|
|
||||||
describe("Tree", () => {
|
describe("Tree", () => {
|
||||||
let rootNote!: NoteBuilder;
|
let rootNote!: NoteBuilder;
|
||||||
@@ -73,4 +74,43 @@ describe("Tree", () => {
|
|||||||
expect(order).toStrictEqual(expectedOrder);
|
expect(order).toStrictEqual(expectedOrder);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pins to the top and bottom", () => {
|
||||||
|
const note = buildNote({
|
||||||
|
children: [
|
||||||
|
{ title: "bottom", "#bottom": "" },
|
||||||
|
{ title: "5" },
|
||||||
|
{ title: "3" },
|
||||||
|
{ title: "2" },
|
||||||
|
{ title: "1" },
|
||||||
|
{ title: "top", "#top": "" }
|
||||||
|
],
|
||||||
|
"#sorted": ""
|
||||||
|
});
|
||||||
|
cls.init(() => {
|
||||||
|
tree.sortNotesIfNeeded(note.noteId);
|
||||||
|
});
|
||||||
|
const orderedTitles = note.children.map((child) => child.title);
|
||||||
|
expect(orderedTitles).toStrictEqual([ "top", "1", "2", "3", "5", "bottom" ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pins to the top and bottom in reverse order", () => {
|
||||||
|
const note = buildNote({
|
||||||
|
children: [
|
||||||
|
{ title: "bottom", "#bottom": "" },
|
||||||
|
{ title: "1" },
|
||||||
|
{ title: "2" },
|
||||||
|
{ title: "3" },
|
||||||
|
{ title: "5" },
|
||||||
|
{ title: "top", "#top": "" }
|
||||||
|
],
|
||||||
|
"#sorted": "",
|
||||||
|
"#sortDirection": "desc"
|
||||||
|
});
|
||||||
|
cls.init(() => {
|
||||||
|
tree.sortNotesIfNeeded(note.noteId);
|
||||||
|
});
|
||||||
|
const orderedTitles = note.children.map((child) => child.title);
|
||||||
|
expect(orderedTitles).toStrictEqual([ "top", "5", "3", "2", "1", "bottom" ]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
|
|||||||
const topBEl = fetchValue(b, "top");
|
const topBEl = fetchValue(b, "top");
|
||||||
|
|
||||||
if (topAEl !== topBEl) {
|
if (topAEl !== topBEl) {
|
||||||
if (topAEl === null) return 1;
|
if (topAEl === null) return reverse ? -1 : 1;
|
||||||
if (topBEl === null) return -1;
|
if (topBEl === null) return reverse ? 1 : -1;
|
||||||
|
|
||||||
// since "top" should not be reversible, we'll reverse it once more to nullify this effect
|
// since "top" should not be reversible, we'll reverse it once more to nullify this effect
|
||||||
return compare(topAEl, topBEl) * (reverse ? -1 : 1);
|
return compare(topAEl, topBEl) * (reverse ? -1 : 1);
|
||||||
@@ -147,8 +147,8 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
|
|||||||
const bottomBEl = fetchValue(b, "bottom");
|
const bottomBEl = fetchValue(b, "bottom");
|
||||||
|
|
||||||
if (bottomAEl !== bottomBEl) {
|
if (bottomAEl !== bottomBEl) {
|
||||||
if (bottomAEl === null) return -1;
|
if (bottomAEl === null) return reverse ? 1 : -1;
|
||||||
if (bottomBEl === null) return 1;
|
if (bottomBEl === null) return reverse ? -1 : 1;
|
||||||
|
|
||||||
// since "bottom" should not be reversible, we'll reverse it once more to nullify this effect
|
// since "bottom" should not be reversible, we'll reverse it once more to nullify this effect
|
||||||
return compare(bottomBEl, bottomAEl) * (reverse ? -1 : 1);
|
return compare(bottomBEl, bottomAEl) * (reverse ? -1 : 1);
|
||||||
|
|||||||
@@ -342,8 +342,11 @@ async function registerGlobalShortcuts() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// window may be hidden / not in focus
|
if (action.actionName === "toggleTray") {
|
||||||
|
targetWindow.focus();
|
||||||
|
} else {
|
||||||
showAndFocusWindow(targetWindow);
|
showAndFocusWindow(targetWindow);
|
||||||
|
}
|
||||||
|
|
||||||
targetWindow.webContents.send("globalShortcut", action.actionName);
|
targetWindow.webContents.send("globalShortcut", action.actionName);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"preact-iso": "2.11.0",
|
"preact-iso": "2.11.0",
|
||||||
"preact-render-to-string": "6.6.3",
|
"preact-render-to-string": "6.6.3",
|
||||||
"react-i18next": "16.2.4"
|
"react-i18next": "16.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "2.10.2",
|
"@preact/preset-vite": "2.10.2",
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
{
|
{
|
||||||
"hero_section": {
|
"hero_section": {
|
||||||
"github": "깃허브",
|
"github": "깃허브",
|
||||||
"dockerhub": "도커 허브"
|
"dockerhub": "도커 허브",
|
||||||
|
"get_started": "시작하기",
|
||||||
|
"title": "생각을 정리하고, 개인 지식 기반을 구축하세요.",
|
||||||
|
"subtitle": "Trilium은 개인 지식 베이스를 정리하고 노트를 작성하는 오픈소스 솔루션입니다. 데스크톱에서 로컬로 사용하거나, 자체 호스팅 서버와 동기화하여 어디에서나 노트를 보관할 수 있습니다.",
|
||||||
|
"screenshot_alt": "Trilium Notes 데스크톱 애플리케이션의 스크린샷"
|
||||||
|
},
|
||||||
|
"get-started": {
|
||||||
|
"title": "시작하기",
|
||||||
|
"desktop_title": "데스크탑 애플리케이션 내려받기 (v{{version}})",
|
||||||
|
"older_releases": "오래된 릴리즈 보기",
|
||||||
|
"architecture": "아키텍쳐:",
|
||||||
|
"server_title": "여러 기기에서 액세스할 수 있는 서버 설정"
|
||||||
|
},
|
||||||
|
"download_now": {
|
||||||
|
"text": "지금 내려받기 "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
"mermaid_description": "Twórz diagramy, takie jak schematy blokowe, diagramy klas i sekwencyjne, wykresy Gantta i wiele innych, korzystając z składni Mermaid.",
|
"mermaid_description": "Twórz diagramy, takie jak schematy blokowe, diagramy klas i sekwencyjne, wykresy Gantta i wiele innych, korzystając z składni Mermaid.",
|
||||||
"mindmap_title": "Mapy myśli",
|
"mindmap_title": "Mapy myśli",
|
||||||
"mindmap_description": "Organizuj wizualnie swoje myśli albo przeprowadź sesję burzy mózgów.",
|
"mindmap_description": "Organizuj wizualnie swoje myśli albo przeprowadź sesję burzy mózgów.",
|
||||||
"others_list": "I wiele innych: <0>mapa notatek</0>, <1>mapa powiązań</1>, <2>zapisane wyszukiwania</2>, <3>renderowane notatki</3>, and <4>podgląd stron www</4>.",
|
"others_list": "I wiele innych: <0>mapa notatek</0>, <1>mapa powiązań</1>, <2>zapisane wyszukiwania</2>, <3>renderowane notatki</3> i <4>podgląd stron www</4>.",
|
||||||
"title": "Wiele sposobów przedstawienia Twoich informacji"
|
"title": "Wiele sposobów przedstawienia Twoich informacji"
|
||||||
},
|
},
|
||||||
"extensibility_benefits": {
|
"extensibility_benefits": {
|
||||||
|
|||||||
@@ -51,7 +51,8 @@
|
|||||||
"mermaid_description": "Creați diagrame precum flowchart-uri, diagrame de secvență sau de clase, Gantt și multe altele, folosind sintaxa Mermaid.",
|
"mermaid_description": "Creați diagrame precum flowchart-uri, diagrame de secvență sau de clase, Gantt și multe altele, folosind sintaxa Mermaid.",
|
||||||
"mindmap_title": "Hartă mentală",
|
"mindmap_title": "Hartă mentală",
|
||||||
"mindmap_description": "Organizați-vă gândurile vizual sau organizați o sesiune de brainstorming.",
|
"mindmap_description": "Organizați-vă gândurile vizual sau organizați o sesiune de brainstorming.",
|
||||||
"others_list": "și altele: <0>hartă a notițelor</0>, <1>hartă a relațiilor</1>, <2>căutări salvate</2>, <3>randare a notițelor</3>, și <4>vizualizări web</4>."
|
"others_list": "și altele: <0>hartă a notițelor</0>, <1>hartă a relațiilor</1>, <2>căutări salvate</2>, <3>randare a notițelor</3>, și <4>vizualizări web</4>.",
|
||||||
|
"title": "Multiple modalități de a reprezenta informația"
|
||||||
},
|
},
|
||||||
"extensibility_benefits": {
|
"extensibility_benefits": {
|
||||||
"title": "Partajare și extensibilitate",
|
"title": "Partajare și extensibilitate",
|
||||||
@@ -72,7 +73,10 @@
|
|||||||
"board_title": "Tabelă Kanban",
|
"board_title": "Tabelă Kanban",
|
||||||
"board_description": "Organizați-vă sarcinile sau proiectele într-o tabelă Kanban cu o modalitate ușoară de a adăuga elemente și coloane noi și schimbarea stării acestora prin glisare cu mouse-ul.",
|
"board_description": "Organizați-vă sarcinile sau proiectele într-o tabelă Kanban cu o modalitate ușoară de a adăuga elemente și coloane noi și schimbarea stării acestora prin glisare cu mouse-ul.",
|
||||||
"geomap_title": "Hartă geografică",
|
"geomap_title": "Hartă geografică",
|
||||||
"geomap_description": "Planificați-vă vacanțele sau marcați-vă punctele de interes direct pe o hartă geografică. Afișați traseele GPX înregistrate pentru a putea urmări itinerarii."
|
"geomap_description": "Planificați-vă vacanțele sau marcați-vă punctele de interes direct pe o hartă geografică. Afișați traseele GPX înregistrate pentru a putea urmări itinerarii.",
|
||||||
|
"title": "Colecții",
|
||||||
|
"presentation_title": "Prezentare",
|
||||||
|
"presentation_description": "Organizați informația în diapozitive și prezentați-le pe tot ecranul, cu tranziții fine. Diapozitivele pot fi ulterior exportate ca PDF pentru o partajare ușoară."
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"title": "Întrebări frecvente",
|
"title": "Întrebări frecvente",
|
||||||
|
|||||||
67
docs/Developer Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 2,
|
"formatVersion": 2,
|
||||||
"appVersion": "0.99.4",
|
"appVersion": "0.99.5",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"isClone": false,
|
"isClone": false,
|
||||||
@@ -110,6 +110,13 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"mime": "text/html",
|
"mime": "text/html",
|
||||||
"attributes": [
|
"attributes": [
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "4nwtTJyjNDKd",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 10
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "label",
|
"type": "label",
|
||||||
"name": "iconClass",
|
"name": "iconClass",
|
||||||
@@ -117,13 +124,6 @@
|
|||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 20
|
"position": 20
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "4nwtTJyjNDKd",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 30
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "label",
|
"type": "label",
|
||||||
"name": "shareAlias",
|
"name": "shareAlias",
|
||||||
@@ -1263,10 +1263,17 @@
|
|||||||
{
|
{
|
||||||
"type": "relation",
|
"type": "relation",
|
||||||
"name": "internalLink",
|
"name": "internalLink",
|
||||||
"value": "zdQzavvHDl1k",
|
"value": "ccIoz7nqgDRK",
|
||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 10
|
"position": 10
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "zdQzavvHDl1k",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 20
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "label",
|
"type": "label",
|
||||||
"name": "iconClass",
|
"name": "iconClass",
|
||||||
@@ -1280,13 +1287,6 @@
|
|||||||
"value": "releasing",
|
"value": "releasing",
|
||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 40
|
"position": 40
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "ccIoz7nqgDRK",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 50
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
@@ -1961,6 +1961,13 @@
|
|||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 10
|
"position": 10
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "lXjOyKpUSKgE",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 20
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "label",
|
"type": "label",
|
||||||
"name": "iconClass",
|
"name": "iconClass",
|
||||||
@@ -2071,6 +2078,34 @@
|
|||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"dataFileName": "Server translations.md",
|
"dataFileName": "Server translations.md",
|
||||||
"attachments": []
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"isClone": false,
|
||||||
|
"noteId": "lXjOyKpUSKgE",
|
||||||
|
"notePath": [
|
||||||
|
"jdjRLhLV3TtI",
|
||||||
|
"yeqU0zo0ZQ83",
|
||||||
|
"TLXJwBDo8Rdv",
|
||||||
|
"lXjOyKpUSKgE"
|
||||||
|
],
|
||||||
|
"title": "Adding a new locale",
|
||||||
|
"notePosition": 40,
|
||||||
|
"prefix": null,
|
||||||
|
"isExpanded": false,
|
||||||
|
"type": "text",
|
||||||
|
"mime": "text/html",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"name": "shareAlias",
|
||||||
|
"value": "new-locale",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": "markdown",
|
||||||
|
"dataFileName": "Adding a new locale.md",
|
||||||
|
"attachments": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ Follow the <a class="reference-link" href="Internationalisation%20%20Translatio
|
|||||||
|
|
||||||
### Adding a new locale
|
### Adding a new locale
|
||||||
|
|
||||||
To add a new locale, go to `src/public/translations` with your favorite text editor and copy the `en` directory.
|
See <a class="reference-link" href="Internationalisation%20%20Translations/Adding%20a%20new%20locale.md">Adding a new locale</a>.
|
||||||
|
|
||||||
Rename the copy to the ISO code (e.g. `fr`, `ro`) of the language being translated.
|
|
||||||
|
|
||||||
Translations with a country-language combination, using their corresponding ISO code (e.g. `fr_FR`, `fr_BE`), has not been tested yet.
|
|
||||||
|
|
||||||
### Changing the language
|
### Changing the language
|
||||||
|
|
||||||
|
|||||||