Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
f19d6e8daa chore(deps): bump js-yaml from 3.13.1 to 4.1.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.13.1 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.13.1...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 06:58:51 +00:00
103 changed files with 602 additions and 1754 deletions

View File

@@ -155,10 +155,6 @@ jobs:
- name: 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
run: pnpm run server:build

View File

@@ -57,7 +57,7 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Update nightly version
run: pnpm run chore:ci-update-nightly-version
run: npm run chore:ci-update-nightly-version
- name: Run the build
uses: ./.github/actions/build-electron
with:

View File

@@ -39,14 +39,14 @@
"@stylistic/eslint-plugin": "5.5.0",
"@types/express": "5.0.5",
"@types/node": "24.10.1",
"@types/yargs": "17.0.35",
"@types/yargs": "17.0.34",
"@vitest/coverage-v8": "3.2.4",
"eslint": "9.39.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",
"jsdoc": "4.0.5",
"lorem-ipsum": "2.0.8",
"rcedit": "5.0.1",
"rcedit": "5.0.0",
"rimraf": "6.1.0",
"tslib": "2.8.1"
},

View File

@@ -36,7 +36,7 @@
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"color": "5.0.3",
"color": "5.0.2",
"dayjs": "1.11.19",
"dayjs-plugin-utc": "0.1.2",
"debounce": "3.0.0",
@@ -55,7 +55,7 @@
"mark.js": "8.11.1",
"marked": "16.4.2",
"mermaid": "11.12.1",
"mind-elixir": "5.3.6",
"mind-elixir": "5.3.5",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.27.2",

View File

@@ -647,32 +647,7 @@ export default class TabManager extends Component {
...this.noteContexts.slice(-noteContexts.length),
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
];
// 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
});
this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null) });
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
if (mainNtx) {

View File

@@ -257,9 +257,7 @@ export default class FNote {
}
async getChildNoteIdsWithArchiveFiltering(includeArchived = false) {
const isHiddenNote = this.noteId.startsWith("_");
const isSearchNote = this.type === "search";
if (!includeArchived && !isHiddenNote && !isSearchNote) {
if (!includeArchived) {
const unorderedIds = new Set(await search.searchForNoteIds(`note.parents.noteId="${this.noteId}" #!archived`));
const results: string[] = [];
for (const id of this.children) {
@@ -806,16 +804,6 @@ export default class FNote {
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
* @returns relation value if relation exists, null otherwise

View File

@@ -22,15 +22,6 @@ 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) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}
@@ -60,23 +51,6 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
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.
* For an attribute with an empty value, pass an empty string instead.
@@ -142,10 +116,8 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
export default {
addLabel,
setLabel,
setRelation,
setAttribute,
removeAttributeById,
removeOwnedLabelByName,
removeOwnedRelationByName,
isAffecting
};

View File

@@ -1,5 +1,4 @@
import type FNote from "../entities/fnote.js";
import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_helper.js";
import { getCurrentLanguage } from "./i18n.js";
import { formatCodeBlocks } from "./syntax_highlight.js";
@@ -11,18 +10,18 @@ export default function renderDoc(note: FNote) {
if (docName) {
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
$content.load(url, async (response, status) => {
$content.load(url, (response, status) => {
// fallback to english doc if no translation available
if (status === "error") {
const fallbackUrl = getUrl(docName, "en");
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content)
$content.load(fallbackUrl, () => {
processContent(fallbackUrl, $content)
resolve($content);
});
return;
}
await processContent(url, $content);
processContent(url, $content);
resolve($content);
});
} else {
@@ -33,7 +32,7 @@ export default function renderDoc(note: FNote) {
});
}
async function processContent(url: string, $content: JQuery<HTMLElement>) {
function processContent(url: string, $content: JQuery<HTMLElement>) {
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.
@@ -43,9 +42,6 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
});
formatCodeBlocks($content);
// Apply reference links.
await applyReferenceLinks($content[0]);
}
function getUrl(docNameValue: string, language: string) {

View File

@@ -29,8 +29,6 @@ async function getActionsForScope(scope: string) {
}
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
if (!$el[0]) return [];
const actions = await getActionsForScope(scope);
const bindings: ShortcutBinding[] = [];

View File

@@ -307,8 +307,7 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
// Right click is handled separately.
const isMiddleClick = evt && "which" in evt && evt.which === 2;
const targetIsBlank = ($link?.attr("target") === "_blank");
const isDoubleClick = isLeftClick && evt?.type === "dblclick";
const openInNewTab = (isLeftClick && ctrlKey) || isDoubleClick || isMiddleClick || targetIsBlank;
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
@@ -329,18 +328,16 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
if (openInNewTab || openInNewWindow || (isLeftClick && (withinEditLink || outsideOfCKEditor))) {
if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
window.open(hrefLink, "_blank");
} else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
const electron = utils.dynamicRequire("electron");
electron.shell.openPath(hrefLink);
} else {
// Enable protocols supported by CKEditor 5 to be clickable.
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");
}
}
}
@@ -476,9 +473,18 @@ $(document).on("auxclick", "a", goToLink); // to handle the middle button
// TODO: Check why the event is not supported.
//@ts-ignore
$(document).on("contextmenu", "a", linkContextMenu);
// TODO: Check why the event is not supported.
//@ts-ignore
$(document).on("dblclick", "a", goToLink);
$(document).on("dblclick", "a", (e) => {
e.preventDefault();
e.stopPropagation();
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) => {
if (e.which === 2) {

View File

@@ -1,21 +1,11 @@
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
import type { AttachmentRow, EtapiTokenRow, OptionNames } from "@triliumnext/commons";
import type { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js";
// TODO: Deduplicate with server.
interface NoteRow {
blobId: string;
dateCreated: string;
dateModified: string;
isDeleted?: boolean;
isProtected?: boolean;
mime: string;
noteId: string;
title: string;
type: NoteType;
utcDateCreated: string;
utcDateModified: string;
}
// TODO: Deduplicate with BranchRow from `rows.ts`/

View File

@@ -77,11 +77,11 @@ function closePersistent(id: string) {
$(`#toast-${id}`).remove();
}
function showMessage(message: string, delay = 2000, icon = "check") {
function showMessage(message: string, delay = 2000) {
console.debug(utils.now(), "message:", message);
toast({
icon,
icon: "check",
message: message,
autohide: true,
delay

View File

@@ -16,10 +16,6 @@
background-color: var(--root-background);
}
body.mobile #root-widget {
background-color: var(--main-background-color);
}
body {
--native-titlebar-darwin-x-offset: 10;
--native-titlebar-darwin-y-offset: 12 !important;

View File

@@ -2035,8 +2035,7 @@
"add-column": "Add Column",
"add-column-placeholder": "Enter column name...",
"edit-note-title": "Click to edit note title",
"edit-column-title": "Click to edit column title",
"column-already-exists": "This column already exists on the board."
"edit-column-title": "Click to edit column title"
},
"presentation_view": {
"edit-slide": "Edit this slide",

View File

@@ -302,10 +302,7 @@
"edit_branch_prefix": "Editează prefixul ramurii",
"help_on_tree_prefix": "Informații despre prefixe de ierarhie",
"prefix": "Prefix: ",
"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}}):"
"save": "Salvează"
},
"bulk_actions": {
"affected_notes": "Notițe afectate",
@@ -540,8 +537,7 @@
"opml_version_1": "OPML v1.0 - text simplu",
"opml_version_2": "OPML v2.0 - permite și HTML",
"format_html": "HTML - recomandat deoarece păstrează toata formatarea",
"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."
"format_pdf": "PDF - cu scopul de printare sau partajare."
},
"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.",
@@ -757,8 +753,7 @@
"placeholder": "Introduceți etichetele HTML, câte unul pe linie",
"reset_button": "Resetează la lista implicită",
"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 notițele arhivate"
@@ -804,8 +799,7 @@
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
"max_width_label": "Lungimea maximă a conținutului",
"max_width_unit": "pixeli",
"title": "Lățime conținut",
"centerContent": "Centrează conținutul"
"title": "Lățime conținut"
},
"mobile_detail_menu": {
"delete_this_note": "Șterge această notiță",
@@ -862,8 +856,7 @@
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
"convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.",
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?",
"print_pdf": "Exportare ca PDF...",
"open_note_on_server": "Deschide notița pe server"
"print_pdf": "Exportare ca PDF..."
},
"note_erasure_timeout": {
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
@@ -1253,11 +1246,11 @@
"timeout_unit": "milisecunde"
},
"table_of_contents": {
"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:",
"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:",
"unit": "titluri",
"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 cuprinsul) în Opțiuni -> Scurtături (denumirea „toggleRightPane”).",
"title": "Cuprins"
"disable_info": "De asemenea se poate dezactiva tabela de conținut 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”).",
"title": "Tabelă de conținut"
},
"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ță).",
@@ -1510,9 +1503,7 @@
"window-on-top": "Menține fereastra mereu vizibilă"
},
"note_detail": {
"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..."
"could_not_find_typewidget": "Nu s-a putut găsi widget-ul corespunzător tipului „{{type}}”"
},
"note_title": {
"placeholder": "introduceți titlul notiței aici..."
@@ -2023,8 +2014,7 @@
"new-item-placeholder": "Introduceți titlul notiței...",
"add-column-placeholder": "Introduceți denumirea coloanei...",
"edit-note-title": "Clic pentru a edita titlul notiței",
"edit-column-title": "Clic pentru a edita titlul coloanei",
"column-already-exists": "Această coloană deja există."
"edit-column-title": "Clic pentru a edita titlul coloanei"
},
"command_palette": {
"tree-action-name": "Listă de notițe: {{name}}",
@@ -2086,14 +2076,5 @@
"edit-slide": "Editați acest slide",
"start-presentation": "Începeți prezentarea",
"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..."
}
}

View File

@@ -1,4 +1,4 @@
.user-attributes {
.promoted-attributes {
display: flex;
flex-wrap: wrap;
gap: 8px;
@@ -6,7 +6,7 @@
margin-top: 8px;
}
.user-attributes .user-attribute {
.promoted-attributes .promoted-attribute {
padding: 2px 10px;
border-radius: 9999px;
white-space: nowrap;
@@ -17,15 +17,15 @@
line-height: 1.2;
}
.user-attributes .user-attribute:hover {
.promoted-attributes .promoted-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 {
.promoted-attributes .promoted-attribute .name {
font-weight: 600;
}
.user-attributes .user-attribute .value {
.promoted-attributes .promoted-attribute .value {
opacity: 0.9;
}

View File

@@ -1,16 +1,16 @@
import { useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import "./UserAttributesList.css";
import "./PromotedAttributesDisplay.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 { ComponentChild, ComponentChildren, CSSProperties } from "preact";
import Icon from "../react/Icon";
import NoteLink from "../react/NoteLink";
import { getReadableTextColor } from "../../services/css_class_manager";
interface UserAttributesListProps {
interface PromotedAttributesDisplayProps {
note: FNote;
ignoredAttributes?: string[];
}
@@ -23,39 +23,39 @@ interface AttributeWithDefinitions {
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))}
export default function PromotedAttributesDisplay({ note, ignoredAttributes }: PromotedAttributesDisplayProps) {
const promotedDefinitionAttributes = useNoteAttributesWithDefinitions(note, ignoredAttributes);
return promotedDefinitionAttributes?.length > 0 && (
<div className="promoted-attributes">
{promotedDefinitionAttributes?.map(attr => buildPromotedAttribute(attr))}
</div>
)
}
function useNoteAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {
const [ userAttributes, setUserAttributes ] = useState<AttributeWithDefinitions[]>(getAttributesWithDefinitions(note, attributesToIgnore));
const [ promotedDefinitionAttributes, setPromotedDefinitionAttributes ] = useState<AttributeWithDefinitions[]>(getAttributesWithDefinitions(note, attributesToIgnore));
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setUserAttributes(getAttributesWithDefinitions(note, attributesToIgnore));
setPromotedDefinitionAttributes(getAttributesWithDefinitions(note, attributesToIgnore));
}
});
return userAttributes;
return promotedDefinitionAttributes;
}
function UserAttribute({ attr, children, style }: { attr: AttributeWithDefinitions, children: ComponentChildren, style?: CSSProperties }) {
function PromotedAttribute({ 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}>
<span key={attr.friendlyName} className={`promoted-attribute type-${className}`} style={style}>
{children}
</span>
)
}
function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
function buildPromotedAttribute(attr: AttributeWithDefinitions): ComponentChildren {
const defaultLabel = <><strong>{attr.friendlyName}:</strong>{" "}</>;
let content: ComponentChildren;
let style: CSSProperties | undefined;
@@ -102,13 +102,13 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
content = <>{defaultLabel}<NoteLink notePath={attr.value} showNoteIcon /></>;
}
return <UserAttribute attr={attr} style={style}>{content}</UserAttribute>
return <PromotedAttribute attr={attr} style={style}>{content}</PromotedAttribute>
}
function getAttributesWithDefinitions(note: FNote, attributesToIgnore: string[] = []): AttributeWithDefinitions[] {
const attributeDefintions = note.getAttributeDefinitions();
const promotedDefinitionAttributes = note.getAttributeDefinitions();
const result: AttributeWithDefinitions[] = [];
for (const attr of attributeDefintions) {
for (const attr of promotedDefinitionAttributes) {
const def = attr.getDefinition();
const [ type, name ] = attr.name.split(":", 2);
const friendlyName = def?.promotedAlias || name;

View File

@@ -1,20 +1,18 @@
import { useEffect, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import ActionButton from "../react/ActionButton";
import { useNoteContext, useTriliumEvents } from "../react/hooks";
import appContext from "../../components/app_context";
import { useNoteContext, useTriliumEvent } from "../react/hooks";
export default function ClosePaneButton() {
const { noteContext, ntxId, parentComponent } = useNoteContext();
const [isEnabled, setIsEnabled] = useState(false);
const [ isEnabled, setIsEnabled ] = useState(false);
function refresh() {
const isMainOfSomeContext = appContext.tabManager.noteContexts.some(c => c.mainNtxId === ntxId);
setIsEnabled(!!(noteContext && (!!noteContext.mainNtxId || isMainOfSomeContext)));
setIsEnabled(!!(noteContext && !!noteContext.mainNtxId));
}
useTriliumEvents(["noteContextRemoved", "noteContextReorder", "newNoteContextCreated"], refresh);
useEffect(refresh, [ntxId]);
useTriliumEvent("noteContextReorder", refresh);
useEffect(refresh, [ ntxId ]);
return (
<ActionButton

View File

@@ -77,8 +77,8 @@ export function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable
props = {
note, noteIds, notePath,
highlightedTokens,
viewConfig: viewModeConfig.config,
saveConfig: viewModeConfig.storeFn,
viewConfig: viewModeConfig[0],
saveConfig: viewModeConfig[1],
onReady: onReady ?? (() => {}),
...restProps
}
@@ -192,11 +192,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
}
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
const [ viewConfig, setViewConfig ] = useState<{
config: T | undefined;
storeFn: (data: T) => void;
note: FNote;
}>();
const [ viewConfig, setViewConfig ] = useState<[T | undefined, (data: T) => void]>();
useEffect(() => {
if (!note || !viewType) return;
@@ -204,14 +200,12 @@ export function useViewModeConfig<T extends object>(note: FNote | null | undefin
const viewStorage = new ViewModeStorage<T>(note, viewType);
viewStorage.restore().then(config => {
const storeFn = (config: T) => {
setViewConfig({ note, config, storeFn });
setViewConfig([ config, storeFn ]);
viewStorage.store(config);
};
setViewConfig({ note, config, storeFn });
setViewConfig([ config, storeFn ]);
});
}, [ note, viewType ]);
// Only expose config for the current note, avoid leaking notes when switching between them.
if (viewConfig?.note !== note) return undefined;
return viewConfig;
}

View File

@@ -1,4 +1,3 @@
import { BulkAction } from "@triliumnext/commons";
import { BoardViewData } from ".";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
@@ -13,25 +12,15 @@ import { ColumnMap } from "./data";
export default class BoardApi {
private isRelationMode: boolean;
statusAttribute: string;
constructor(
private byColumn: ColumnMap | undefined,
public columns: string[],
private parentNote: FNote,
statusAttribute: string,
readonly statusAttribute: string,
private viewConfig: BoardViewData,
private saveConfig: (newConfig: BoardViewData) => 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) {
try {
@@ -53,11 +42,7 @@ export default class BoardApi {
}
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) {
@@ -75,20 +60,22 @@ export default class BoardApi {
// Add the new column to persisted data if it doesn't exist
const existingColumn = this.viewConfig.columns.find(col => col.value === columnName);
if (existingColumn) return false;
this.viewConfig.columns.push({ value: columnName });
this.saveConfig(this.viewConfig);
return true;
if (!existingColumn) {
this.viewConfig.columns.push({ value: columnName });
this.saveConfig(this.viewConfig);
}
}
async removeColumn(column: string) {
// Remove the value from the notes.
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.saveConfig(this.viewConfig);
}
@@ -97,10 +84,13 @@ export default class BoardApi {
const noteIds = this.byColumn?.get(oldValue)?.map(item => item.note.noteId) || [];
// Change the value in the notes.
const action: BulkAction = this.isRelationMode
? { name: "updateRelationTarget", relationName: this.statusAttribute, targetNoteId: newValue }
: { name: "updateLabelValue", labelName: this.statusAttribute, labelValue: newValue }
await executeBulkActions(noteIds, [ action ]);
await executeBulkActions(noteIds, [
{
name: "updateLabelValue",
labelName: this.statusAttribute,
labelValue: newValue
}
]);
// Rename the column in the persisted data.
for (const column of this.viewConfig.columns || []) {
@@ -177,11 +167,7 @@ export default class BoardApi {
removeFromBoard(noteId: string) {
const note = froca.getNoteFromCache(noteId);
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) {

View File

@@ -6,8 +6,7 @@ import { BoardViewContext, TitleEditor } from ".";
import { ContextMenuEvent } from "../../../menus/context_menu";
import { openNoteContextMenu } from "./context_menu";
import { t } from "../../../services/i18n";
import UserAttributesDisplay from "../../attribute_widgets/UserAttributesList";
import { useTriliumEvent } from "../../react/hooks";
import PromotedAttributesDisplay from "../../attribute_widgets/PromotedAttributesDisplay";
export const CARD_CLIPBOARD_TYPE = "trilium/board-card";
@@ -41,13 +40,6 @@ export default function Card({
const [ isVisible, setVisible ] = useState(true);
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) => {
e.dataTransfer!.effectAllowed = 'move';
const data: CardDragData = { noteId: note.noteId, branchId: branch.branchId, fromColumn: column, index };
@@ -117,7 +109,7 @@ export default function Card({
title={t("board_view.edit-note-title")}
onClick={handleEdit}
/>
<UserAttributesDisplay note={note} ignoredAttributes={[api.statusAttribute]} />
<PromotedAttributesDisplay note={note} ignoredAttributes={[api.statusAttribute]} />
</>
) : (
<TitleEditor
@@ -127,7 +119,7 @@ export default function Card({
setTitle(newTitle);
}}
dismiss={() => api.dismissEditingTitle()}
mode="multiline"
multiline
/>
)}
</div>

View File

@@ -12,7 +12,6 @@ import Card, { CARD_CLIPBOARD_TYPE, CardDragData } from "./card";
import { JSX } from "preact/jsx-runtime";
import froca from "../../../services/froca";
import { DragData, TREE_CLIPBOARD_TYPE } from "../../note_tree";
import NoteLink from "../../react/NoteLink";
interface DragContext {
column: string;
@@ -28,14 +27,12 @@ export default function Column({
api,
onColumnHover,
isAnyColumnDragging,
isInRelationMode
}: {
columnItems?: { note: FNote, branch: FBranch }[];
isDraggingColumn: boolean,
api: BoardApi,
onColumnHover?: (index: number, mouseX: number, rect: DOMRect) => void,
isAnyColumnDragging?: boolean,
isInRelationMode: boolean
isAnyColumnDragging?: boolean
} & DragContext) {
const [ isVisible, setVisible ] = useState(true);
const { columnNameToEdit, setColumnNameToEdit, dropTarget, draggedCard, dropPosition } = useContext(BoardViewContext)!;
@@ -106,11 +103,7 @@ export default function Column({
>
{!isEditing ? (
<>
<span className="title">
{isInRelationMode
? <NoteLink notePath={column} showNoteIcon />
: column}
</span>
<span className="title">{column}</span>
<span className="counter-badge">{columnItems?.length ?? 0}</span>
<div className="spacer" />
<span
@@ -124,7 +117,6 @@ export default function Column({
currentValue={column}
save={newTitle => api.renameColumn(column, newTitle)}
dismiss={() => setColumnNameToEdit?.(undefined)}
mode={isInRelationMode ? "relation" : "normal"}
/>
)}
</h3>
@@ -188,7 +180,7 @@ function AddNewItem({ column, api }: { column: string, api: BoardApi }) {
placeholder={t("board_view.new-item-placeholder")}
save={(title) => api.createNewItem(column, title)}
dismiss={() => setIsCreatingNewItem(false)}
mode="multiline" isNewItem
multiline isNewItem
/>
)}
</div>

View File

@@ -57,8 +57,7 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
return {
byColumn,
newPersistedData,
isInRelationMode: groupByColumn.startsWith("~")
newPersistedData
};
}
@@ -71,7 +70,7 @@ async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupB
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived, seenNoteIds);
}
const group = note.getLabelOrRelation(groupByColumn);
const group = note.getLabelValue(groupByColumn);
if (!group || seenNoteIds.has(note.noteId)) {
continue;
}

View File

@@ -9,12 +9,6 @@
--card-padding: 0.6em;
}
body.mobile .board-view {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.board-view-container {
height: 100%;
display: flex;
@@ -37,12 +31,6 @@ body.mobile .board-view {
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 {
border-color: var(--main-text-color);
background-color: var(--hover-item-background-color);
@@ -65,11 +53,6 @@ body.mobile .board-view-container .board-column {
align-items: center;
}
.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);

View File

@@ -13,8 +13,6 @@ import Column from "./column";
import BoardApi from "./api";
import FormTextArea from "../../react/FormTextArea";
import FNote from "../../../entities/fnote";
import NoteAutocomplete from "../../react/NoteAutocomplete";
import toast from "../../../services/toast";
export interface BoardViewData {
columns?: BoardColumnData[];
@@ -44,11 +42,10 @@ interface BoardViewContextData {
export const BoardViewContext = createContext<BoardViewContextData | undefined>(undefined);
export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps<BoardViewData>) {
const [ statusAttributeWithPrefix ] = useNoteLabelWithDefault(parentNote, "board:groupBy", "status");
const [ statusAttribute ] = useNoteLabelWithDefault(parentNote, "board:groupBy", "status");
const [ includeArchived ] = useNoteLabelBoolean(parentNote, "includeArchived");
const [ byColumn, setByColumn ] = useState<ColumnMap>();
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 [ dropTarget, setDropTarget ] = useState<string | null>(null);
const [ dropPosition, setDropPosition ] = useState<{ column: string, index: number } | null>(null);
@@ -58,8 +55,8 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
const [ branchIdToEdit, setBranchIdToEdit ] = useState<string>();
const [ columnNameToEdit, setColumnNameToEdit ] = useState<string>();
const api = useMemo(() => {
return new Api(byColumn, columns ?? [], parentNote, statusAttributeWithPrefix, viewConfig ?? {}, saveConfig, setBranchIdToEdit );
}, [ byColumn, columns, parentNote, statusAttributeWithPrefix, viewConfig, saveConfig, setBranchIdToEdit ]);
return new Api(byColumn, columns ?? [], parentNote, statusAttribute, viewConfig ?? {}, saveConfig, setBranchIdToEdit );
}, [ byColumn, columns, parentNote, statusAttribute, viewConfig, saveConfig, setBranchIdToEdit ]);
const boardViewContext = useMemo<BoardViewContextData>(() => ({
api,
parentNote,
@@ -81,9 +78,8 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
]);
function refresh() {
getBoardData(parentNote, statusAttributeWithPrefix, viewConfig ?? {}, includeArchived).then(({ byColumn, newPersistedData, isInRelationMode }) => {
getBoardData(parentNote, statusAttribute, viewConfig ?? {}, includeArchived).then(({ byColumn, newPersistedData }) => {
setByColumn(byColumn);
setIsRelationMode(isInRelationMode);
if (newPersistedData) {
viewConfig = { ...newPersistedData };
@@ -98,7 +94,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
});
}
useEffect(refresh, [ parentNote, noteIds, viewConfig, statusAttributeWithPrefix ]);
useEffect(refresh, [ parentNote, noteIds, viewConfig ]);
const handleColumnDrop = useCallback((fromIndex: number, toIndex: number) => {
const newColumns = api.reorderColumn(fromIndex, toIndex);
@@ -114,7 +110,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
// Check if any changes affect our board
const hasRelevantChanges =
// React to changes in status attribute for notes in this board
loadResults.getAttributeRows().some(attr => attr.name === api.statusAttribute && noteIds.includes(attr.noteId!)) ||
loadResults.getAttributeRows().some(attr => attr.name === statusAttribute && noteIds.includes(attr.noteId!)) ||
// React to changes in note title
loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) ||
// React to changes in branches for subchildren (e.g., moved, added, or removed notes)
@@ -175,7 +171,6 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
<div className="column-drop-placeholder show" />
)}
<Column
isInRelationMode={isInRelationMode}
api={api}
column={column}
columnIndex={index}
@@ -190,14 +185,14 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
<div className="column-drop-placeholder show" />
)}
<AddNewColumn api={api} isInRelationMode={isInRelationMode} />
<AddNewColumn api={api} />
</div>
</BoardViewContext.Provider>
</div>
)
}
function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMode: boolean }) {
function AddNewColumn({ api }: { api: BoardApi }) {
const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false);
const addColumnCallback = useCallback(() => {
@@ -214,28 +209,22 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
: (
<TitleEditor
placeholder={t("board_view.add-column-placeholder")}
save={async (columnName) => {
const created = await api.addNewColumn(columnName);
if (!created) {
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
}
}}
save={(columnName) => api.addNewColumn(columnName)}
dismiss={() => setIsCreatingNewColumn(false)}
isNewItem
mode={isInRelationMode ? "relation" : "normal"}
/>
)}
</div>
)
}
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
export function TitleEditor({ currentValue, placeholder, save, dismiss, multiline, isNewItem }: {
currentValue?: string;
placeholder?: string;
save: (newValue: string) => void;
dismiss: () => void;
multiline?: boolean;
isNewItem?: boolean;
mode?: "normal" | "multiline" | "relation";
}) {
const inputRef = useRef<any>(null);
const focusElRef = useRef<Element>(null);
@@ -243,11 +232,13 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
const shouldDismiss = useRef(false);
useEffect(() => {
focusElRef.current = document.activeElement !== document.body ? document.activeElement : null;
focusElRef.current = document.activeElement;
inputRef.current?.focus();
inputRef.current?.select();
}, [ inputRef ]);
const Element = multiline ? FormTextArea : FormTextBox;
useEffect(() => {
if (dismissOnNextRefreshRef.current) {
dismiss();
@@ -255,62 +246,31 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
}
});
const onKeyDown = (e: TargetedKeyboardEvent<HTMLInputElement | HTMLTextAreaElement> | KeyboardEvent) => {
if (e.key === "Enter" || e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
if (focusElRef.current instanceof HTMLElement) {
shouldDismiss.current = (e.key === "Escape");
focusElRef.current.focus();
} else {
dismiss();
}
}
};
const onBlur = (newValue: string) => {
if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) {
save(newValue);
dismissOnNextRefreshRef.current = true;
} else {
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();
return (
<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") {
e.preventDefault();
e.stopPropagation();
shouldDismiss.current = (e.key === "Escape");
if (focusElRef.current instanceof HTMLElement) {
focusElRef.current.focus();
}
}}
onBlur={() => dismiss()}
noteIdChanged={(newValue) => {
}
}}
onBlur={(newValue) => {
if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) {
save(newValue);
dismissOnNextRefreshRef.current = true;
} else {
dismiss();
}}
/>
);
}
}
}}
/>
);
}

View File

@@ -2,7 +2,6 @@ import FNote from "../../../entities/fnote";
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
import link_context_menu from "../../../menus/link_context_menu";
import branches from "../../../services/branches";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
@@ -19,20 +18,8 @@ export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, par
title: t("calendar_view.delete_note"),
uiIcon: "bx bx-trash",
handler: async () => {
const noteToDelete = await froca.getNote(noteId);
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);
}
const branchId = parentNote.childToBranch[noteId];
await branches.deleteNotes([ branchId ], false, false);
}
}
],

View File

@@ -91,7 +91,6 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const [ hideWeekends ] = useNoteLabelBoolean(note, "calendar:hideWeekends");
const [ weekNumbers ] = useNoteLabelBoolean(note, "calendar:weekNumbers");
const [ calendarView, setCalendarView ] = useNoteLabel(note, "calendar:view");
const [ initialDate ] = useNoteLabel(note, "calendar:initialDate");
const initialView = useRef(calendarView);
const viewSpacedUpdate = useSpacedUpdate(() => setCalendarView(initialView.current));
useResizeObserver(containerRef, () => calendarRef.current?.updateSize());
@@ -135,7 +134,6 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
height="90%"
nowIndicator
handleWindowResize={false}
initialDate={initialDate || undefined}
locale={locale}
{...editingProps}
eventDidMount={eventDidMount}

View File

@@ -100,22 +100,9 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
}
async closeThisNoteSplitCommand({ ntxId }: CommandListenerData<"closeThisNoteSplit">) {
if (!ntxId) return;
const contexts = appContext.tabManager.noteContexts;
const currentIndex = contexts.findIndex((c) => c.ntxId === ntxId);
if (currentIndex === -1) return;
const isRemoveMainContext = contexts[currentIndex].isMainContext();
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]
});
if (ntxId) {
await appContext.tabManager.removeNoteContext(ntxId);
}
await appContext.tabManager.removeNoteContext(ntxId);
}
async moveThisNoteSplitCommand({ ntxId, isMovingLeft }: CommandListenerData<"moveThisNoteSplit">) {
@@ -180,16 +167,12 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
splitService.delNoteSplitResizer(ntxIds);
}
contextsReopenedEvent({ ntxId, mainNtxId, afterNtxId }: EventData<"contextsReopened">) {
if (ntxId !== undefined && afterNtxId !== undefined) {
this.$widget.find(`[data-ntx-id="${ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${afterNtxId}"]`));
} else if (mainNtxId) {
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}"]`));
contextsReopenedEvent({ ntxId, afterNtxId }: EventData<"contextsReopened">) {
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}"]`));
}
async refresh() {

View File

@@ -5,7 +5,7 @@ import type { RefObject } from "preact";
import type { CSSProperties } from "preact/compat";
import { useSyncedRef } from "./hooks";
interface NoteAutocompleteProps {
interface NoteAutocompleteProps {
id?: string;
inputRef?: RefObject<HTMLInputElement>;
text?: string;
@@ -15,15 +15,13 @@ interface NoteAutocompleteProps {
opts?: Omit<Options, "container">;
onChange?: (suggestion: Suggestion | null) => void;
onTextChange?: (text: string) => void;
onKeyDown?: (e: KeyboardEvent) => void;
onBlur?: (newValue: string) => void;
noteIdChanged?: (noteId: string) => void;
noteId?: string;
}
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged, onKeyDown, onBlur }: NoteAutocompleteProps) {
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
const ref = useSyncedRef<HTMLInputElement>(externalInputRef);
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
@@ -59,12 +57,6 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
if (onTextChange) {
$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]);
useEffect(() => {
@@ -89,4 +81,4 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
placeholder={placeholder ?? t("add_link.search_note")} />
</div>
);
}
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "preact/hooks";
import link, { ViewScope } from "../../services/link";
import { useImperativeSearchHighlighlighting, useTriliumEvent } from "./hooks";
import { useImperativeSearchHighlighlighting } from "./hooks";
interface NoteLinkOpts {
className?: string;
@@ -19,11 +19,9 @@ interface 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 noteId = stringifiedNotePath.split("/").at(-1);
const ref = useRef<HTMLSpanElement>(null);
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
const [ noteTitle, setNoteTitle ] = useState<string>();
useEffect(() => {
link.createLink(stringifiedNotePath, {
@@ -32,7 +30,7 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
showNoteIcon,
viewScope
}).then(setJqueryEl);
}, [ stringifiedNotePath, showNotePath, title, viewScope, noteTitle ]);
}, [ stringifiedNotePath, showNotePath, title, viewScope ]);
useEffect(() => {
if (!ref.current || !jqueryEl) return;
@@ -40,16 +38,6 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
highlightSearch(ref.current);
}, [ 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) {
jqueryEl?.css(style);
}

View File

@@ -820,15 +820,12 @@ export default class TabRowWidget extends BasicWidget {
}
contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopened">) {
if (!mainNtxId || tabPosition < 0) {
if (!mainNtxId || !tabPosition) {
// no tab reopened
return;
}
const tabEl = this.getTabById(mainNtxId)[0];
if ( tabEl && tabEl.parentNode ){
tabEl.parentNode.insertBefore(tabEl, this.tabEls[tabPosition]);
}
tabEl.parentNode?.insertBefore(tabEl, this.tabEls[tabPosition]);
}
updateTabById(ntxId: string | null) {

View File

@@ -7,6 +7,7 @@ import { useTriliumEvent } from "../react/hooks";
import { refToJQuerySelector } from "../react/react_utils";
export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
const [ html, setHtml ] = useState<string>();
const initialized = useRef<Promise<void> | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
@@ -14,7 +15,7 @@ export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
if (!note) return;
initialized.current = renderDoc(note).then($content => {
containerRef.current?.replaceChildren(...$content);
setHtml($content.html());
});
}, [ note ]);
@@ -25,9 +26,10 @@ export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
});
return (
<div
ref={containerRef}
<RawHtmlBlock
containerRef={containerRef}
className={`note-detail-doc-content ck-content ${viewScope?.viewMode === "contextual-help" ? "contextual-help" : ""}`}
html={html}
/>
);
}

View File

@@ -1,16 +1,15 @@
import { useCallback, useEffect, useRef } from "preact/hooks";
import { TypeWidgetProps } from "./type_widget";
import { MindElixirData, MindElixirInstance, Operation, Options, default as VanillaMindElixir } from "mind-elixir";
import { MindElixirData, MindElixirInstance, Operation, default as VanillaMindElixir } from "mind-elixir";
import { HTMLAttributes, RefObject } from "preact";
// allow node-menu plugin css to be bundled by webpack
import nodeMenu from "@mind-elixir/node-menu";
import "mind-elixir/style";
import "@mind-elixir/node-menu/dist/style.css";
import "./MindMap.css";
import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents } from "../react/hooks";
import { refToJQuerySelector } from "../react/react_utils";
import utils from "../../services/utils";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
const NEW_TOPIC_NAME = "";
@@ -22,24 +21,6 @@ interface MindElixirProps {
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) {
const apiRef = useRef<MindElixirInstance>(null);
const containerRef = useRef<HTMLDivElement>(null);
@@ -129,14 +110,12 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef: externalApiRef, onChange, editable }: MindElixirProps) {
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
const apiRef = useRef<MindElixirInstance>(null);
const [ locale ] = useTriliumOption("locale");
function reinitialize() {
if (!containerRef.current) return;
const mind = new VanillaMindElixir({
el: containerRef.current,
locale: LOCALE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined,
editable
});
@@ -164,7 +143,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
if (data) {
apiRef.current?.init(data);
}
}, [ editable, locale ]);
}, [ editable ]);
// On change listener.
useEffect(() => {

View File

@@ -1,7 +1,7 @@
import { Excalidraw } from "@excalidraw/excalidraw";
import { TypeWidgetProps } from "../type_widget";
import "@excalidraw/excalidraw/index.css";
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
import { useNoteLabelBoolean } from "../../react/hooks";
import { useCallback, useMemo, useRef } from "preact/hooks";
import { type ExcalidrawImperativeAPI, type AppState } from "@excalidraw/excalidraw/types";
import options from "../../../services/options";
@@ -9,8 +9,6 @@ import "./Canvas.css";
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import { goToLinkExt } from "../../../services/link";
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.
// this avoids making excalidraw load the fonts from an external CDN.
@@ -23,7 +21,6 @@ export default function Canvas({ note, noteContext }: TypeWidgetProps) {
const documentStyle = window.getComputedStyle(document.documentElement);
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
}, []);
const [ locale ] = useTriliumOption("locale");
const persistence = useCanvasPersistence(note, noteContext, apiRef, themeStyle, isReadOnly);
/** Use excalidraw's native zoom instead of the global zoom. */
@@ -61,7 +58,6 @@ export default function Canvas({ note, noteContext }: TypeWidgetProps) {
detectScroll={false}
handleKeyboardGlobally={false}
autoFocus={false}
langCode={LANGUAGE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined}
UIOptions={{
canvasActions: {
saveToActiveFile: false,

View File

@@ -1,29 +0,0 @@
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}.`)
}
}
});
});

View File

@@ -1,19 +0,0 @@
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"
};

View File

@@ -203,7 +203,7 @@ function Font({ title, fontFamilyOption, fontSizeOption }: { title: string, font
<FormTextBoxWithUnit
name="tree-font-size"
type="number" min={50} max={200} step={10}
currentValue={fontSize} onBlur={setFontSize}
currentValue={fontSize} onChange={setFontSize}
unit={t("units.percentage")}
/>
</FormGroup>

View File

@@ -1,10 +1,9 @@
import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat";
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor, TemplateDefinition } from "@triliumnext/ckeditor5";
import { buildConfig, BuildEditorOptions } from "./config";
import { useKeyboardShortcuts, useLegacyImperativeHandlers, useNoteContext, useSyncedRef, useTriliumOption } from "../../react/hooks";
import { useLegacyImperativeHandlers, useSyncedRef } from "../../react/hooks";
import link from "../../../services/link";
import froca from "../../../services/froca";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
export type BoxSize = "small" | "medium" | "full";
@@ -38,11 +37,7 @@ 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) {
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
const watchdogRef = useRef<EditorWatchdog>(null);
const [ uiLanguage ] = useTriliumOption("locale");
const [ editor, setEditor ] = useState<CKTextEditor>();
const { parentComponent } = useNoteContext();
useKeyboardShortcuts("text-detail", containerRef, parentComponent);
useImperativeHandle(editorApi, () => ({
hasSelection() {
@@ -158,7 +153,6 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
const editor = await buildEditor(container, !!isClassicEditor, {
forceGplLicense: false,
isClassicEditor: !!isClassicEditor,
uiLanguage: uiLanguage as DISPLAYABLE_LOCALE_IDS,
contentLanguage: contentLanguage ?? null,
templates
});
@@ -183,7 +177,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
watchdog.create(container);
return () => watchdog.destroy();
}, [ contentLanguage, templates, uiLanguage ]);
}, [ contentLanguage, templates ]);
// React to content changes.
useEffect(() => editor?.setData(content ?? ""), [ editor, content ]);

View File

@@ -196,12 +196,14 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
});
}
useKeyboardShortcuts("text-detail", containerRef, parentComponent);
useTriliumEvent("insertDateTimeToText", ({ ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId) return;
const date = new Date();
const customDateTimeFormat = options.get("customDateTimeFormat");
const dateString = utils.formatDateTime(date, customDateTimeFormat);
console.log("Insert text ", ntxId, eventNtxId, dateString);
addTextToEditor(dateString);
});
useTriliumEvent("addTextToActiveEditor", ({ text }) => {

View File

@@ -17,7 +17,6 @@ import link from "../../../services/link";
import { formatCodeBlocks } from "../../../services/syntax_highlight";
import TouchBar, { TouchBarButton, TouchBarSpacer } from "../../react/TouchBar";
import appContext from "../../../components/app_context";
import { applyReferenceLinks } from "./read_only_helper";
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {
const blob = useNoteBlob(note);
@@ -123,3 +122,10 @@ function applyMath(container: HTMLDivElement) {
renderMathInElement(equation, { trust: true });
}
}
function applyReferenceLinks(container: HTMLDivElement) {
const referenceLinks = container.querySelectorAll<HTMLDivElement>("a.reference-link");
for (const referenceLink of referenceLinks) {
link.loadReferenceLinkTitle($(referenceLink));
}
}

View File

@@ -1,39 +0,0 @@
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);
}
}
});
});

View File

@@ -1,5 +1,5 @@
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO } from "@triliumnext/commons";
import { buildExtraCommands, type EditorConfig, getCkLocale, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
import { ALLOWED_PROTOCOLS, MIME_TYPE_AUTO } from "@triliumnext/commons";
import { buildExtraCommands, type EditorConfig, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
@@ -17,7 +17,6 @@ export const OPEN_SOURCE_LICENSE_KEY = "GPL";
export interface BuildEditorOptions {
forceGplLicense: boolean;
isClassicEditor: boolean;
uiLanguage: DISPLAYABLE_LOCALE_IDS;
contentLanguage: string | null;
templates: TemplateDefinition[];
}
@@ -162,8 +161,9 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags"))
},
removePlugins: getDisabledPlugins(),
...await getCkLocale(opts.uiLanguage)
// This value must be kept in sync with the language defined in webpack.config.js.
language: "en",
removePlugins: getDisabledPlugins()
};
// Set up content language.

View File

@@ -1,13 +0,0 @@
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);
}
}

View File

@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "38.7.0",
"electron": "38.6.0",
"@electron-forge/cli": "7.10.2",
"@electron-forge/maker-deb": "7.10.2",
"@electron-forge/maker-dmg": "7.10.2",

View File

@@ -13,7 +13,7 @@
"devDependencies": {
"@types/better-sqlite3": "7.6.13",
"@types/mime-types": "3.0.1",
"@types/yargs": "17.0.35"
"@types/yargs": "17.0.34"
},
"scripts": {
"dev": "tsx src/main.ts",

View File

@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "38.7.0",
"electron": "38.6.0",
"fs-extra": "11.3.2"
},
"scripts": {

View File

@@ -30,7 +30,7 @@
"node-html-parser": "7.0.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.69.0",
"@anthropic-ai/sdk": "0.68.0",
"@braintree/sanitize-url": "7.1.1",
"@electron/remote": "2.1.3",
"@preact/preset-vite": "2.10.2",
@@ -81,7 +81,7 @@
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "3.1.10",
"electron": "38.7.0",
"electron": "38.6.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
@@ -98,7 +98,7 @@
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.6.2",
"i18next-fs-backend": "2.6.1",
"i18next-fs-backend": "2.6.0",
"image-type": "6.0.0",
"ini": "6.0.0",
"is-animated": "2.0.2",

File diff suppressed because one or more lines are too long

View File

@@ -23,29 +23,6 @@
</ol>
<p>These attributes play a crucial role in organizing, categorizing, and
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&nbsp;<a class="reference-link"
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;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&nbsp;<a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>),
or they can be given meaning through the use of&nbsp;<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
&nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;in
order not to accidentally alter a system attribute (unless intended).</p>
<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
of the&nbsp;<a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>,
@@ -54,14 +31,13 @@
only be viewed.</p>
<p>In the list of attributes, labels are prefixed with the <code>#</code> character
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>&nbsp;create
a form-like editing experience for attributes, which makes it easy to enhancing
the organization and management of attributes</p>
<h2>Multiplicity</h2>
<p>Attributes in Trilium can be "multi-valued", meaning multiple attributes
with the same name can co-exist. This can be combined with&nbsp;<a class="reference-link"
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;to easily add them.</p>
with the same name can co-exist.</p>
<h2>Attribute Definitions and Promoted Attributes</h2>
<p>Special labels create "label/attribute" definitions, enhancing the organization
and management of attributes. For more details, see&nbsp;<a class="reference-link"
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>
<h2>Attribute Inheritance</h2>
<p>Trilium supports attribute inheritance, allowing child notes to inherit
attributes from their parents. For more information, see&nbsp;<a class="reference-link"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,116 +1,31 @@
<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
are displayed prominently in the UI which allow them to be easily viewed
and edited.</p>
<p>One way of seeing promoted attributes is as a kind of form with several
fields. Each field is just regular attribute, the only difference is that
they appear on the note itself.</p>
are considered important and thus are "promoted" onto the main note UI.
See example below:</p>
<p>
<img src="Promoted Attributes_promot.png">
</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
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>
<p>Now, how do we make attribute to appear on the UI?</p>
<h2>Attribute definition</h2>
<p>In order to have promoted attributes, there needs to be a way to define
them.</p>
<figure class="image image-style-align-right image_resized" style="width:38.82%;">
<img style="aspect-ratio:492/346;" src="1_Promoted Attributes_image.png"
width="492" height="346">
</figure>
<p>Technically, attributes are only name-value pairs where both name and
value are strings.</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&nbsp;<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 &amp; 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>Attribute is always name-value pair where both name and value are strings.</p>
<p><em>Attribute definition</em> specifies how should this value be interpreted
- is it just string, or is it a date? Should we allow multiple values or
note? And importantly, should we <em>promote</em> the attribute or not?</p>
<p>
<img src="Promoted Attributes_image.png">
</p>
<p>You can notice tag attribute definition. These "definition" attributes
define how the "value" attributes should behave.</p>
<p>So there's one attribute for value and one for definition. But notice
how an definition attribute can be made <a href="#root/_help_bwZpz2ajCEwO">Inheritable</a>,
meaning that it's also applied to all descendant notes. In this case, the
definition used for the whole sub-tree while "value" attributes are for
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>&nbsp;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>&nbsp;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&nbsp;
<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>
how definition attribute is <a href="#root/_help_bwZpz2ajCEwO">Inheritable</a>,
meaning that it's also applied to all descendant note. So in a way, this
definition is used for the whole subtree while "value" attributes are applied
only for this note.</p>
<h3>Inverse relation</h3>
<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
@@ -118,7 +33,7 @@
This is bidirectional relationship - meaning that if a relation is pointing
from husband to wife then there should be always another relation pointing
from wife to husband.</p>
<p>Another example is with parent-child relationship. Again these always
<p>Another example is with parent - child relationship. Again these always
occur in pairs, but in this case it's not exact same relation - the one
going from parent to child might be called <code>isParentOf</code> and the
other one going from child to parent might be called <code>isChildOf</code>.</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -112,12 +112,6 @@
<td>When present (regardless of value), it will show the number of the week
on the calendar.</td>
</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>
<td><code>#calendar:view</code>
</td>

View File

@@ -1,9 +1,8 @@
<aside class="admonition important">
<p><a class="reference-link" href="#root/_help_zEY4DaJG4YT5">Attributes</a><a class="reference-link"
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a><a class="reference-link"
href="#root/_help_zEY4DaJG4YT5">Attributes</a>Starting with Trilium v0.97.0,
the geo map has been converted from a standalone <a href="#root/_help_KSZ04uQ2D1St">note type</a> to
a type of view for the&nbsp;<a class="reference-link" href="#root/_help_0ESUbbAxVnoK">Note List</a>.&nbsp;</p>
<p>Starting with Trilium v0.97.0, the geo map has been converted from a standalone
<a
href="#root/_help_KSZ04uQ2D1St">note type</a>to a type of view for the&nbsp;<a class="reference-link"
href="#root/_help_0ESUbbAxVnoK">Note List</a>.&nbsp;</p>
</aside>
<figure class="image image-style-align-center">
<img style="aspect-ratio:892/675;" src="9_Geo Map_image.png"
@@ -69,7 +68,7 @@
<td>To create a marker, first navigate to the desired point on the map. Then
press the
<img src="10_Geo Map_image.png">button in the&nbsp;<a href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;(top-right)
area.&nbsp;&nbsp;&nbsp;&nbsp;
area.&nbsp;&nbsp;&nbsp;
<br>
<br>If the button is not visible, make sure the button section is visible
by pressing the chevron button (
@@ -83,7 +82,7 @@
width="1730" height="416">
</td>
<td>Once pressed, the map will enter in the insert mode, as illustrated by
the notification.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
the notification.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br>Simply click the point on the map where to place the marker, or the Escape
key to cancel.</td>
@@ -113,8 +112,7 @@
<li>Right click anywhere on the map, where to place the newly created marker
(and corresponding note).</li>
<li>Select <em>Add a marker at this location</em>.</li>
<li>Enter the name of the ne<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>wly
created note.</li>
<li>Enter the name of the newly created note.</li>
<li>The map should be updated with the new marker.</li>
</ol>
<h3>Adding an existing note on note from the note tree</h3>
@@ -223,10 +221,10 @@ width="1288" height="278">
</figure>
</td>
<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.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
on it and a context menu will show up.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br>Simply click on the first item displaying the coordinates and they will
be copied to clipboard.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
be copied to clipboard.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<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>
@@ -284,7 +282,7 @@ width="1288" height="278">
width="696" height="480">
</td>
<td>The address will be visible in the top-left of the screen, in the place
of the search bar.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
of the search bar.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br>Select the coordinates and copy them into the clipboard.</td>
</tr>
@@ -341,7 +339,7 @@ width="1288" height="278">
width="620" height="530">
</figure>
</td>
<td>When going back to the map, the track should now be visible.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<td>When going back to the map, the track should now be visible.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<br>
<br>The start and end points of the track are indicated by the two blue markers.</td>
</tr>

View File

@@ -1,5 +1,5 @@
<figure class="image">
<img style="aspect-ratio:918/248;" src="2_Kanban Board_image.png"
<img style="aspect-ratio:918/248;" src="Kanban Board_image.png"
width="918" height="248">
</figure>
<p>The Board view presents sub-notes in columns for a Kanban-like experience.
@@ -70,22 +70,7 @@
<li>If there are many notes within the column, move the mouse over the column
and use the mouse wheel to scroll.</li>
</ul>
<h3>Working with the note tree</h3>
<p>It's also possible to add items on the board using the&nbsp;<a class="reference-link"
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</p>
<ol>
<li>Select the desired note in the&nbsp;<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>
<h2>Keyboard interaction</h2>
<p>The board view has mild support for keyboard-based navigation:</p>
<ul>
<li>Use <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> to navigate between
@@ -97,81 +82,16 @@
<li>To dismiss a rename of a note or a column, press <kbd>Escape</kbd>.</li>
</ul>
<h2>Configuration</h2>
<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>
<h3>Grouping by another attribute</h3>
<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
the value being the attribute to use (with or without <code>#</code> attribute
prefix).</p>
<h3>Grouping by relations</h3>
<figure class="image image-style-align-right">
<img style="aspect-ratio:535/245;" src="1_Kanban Board_image.png"
width="535" height="245">
</figure>
<p>A more advanced use-case is grouping by <a href="#root/_help_Cq5X6iKQop6R">Relations</a>.</p>
<p>During this mode:</p>
<ul>
<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>
<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&nbsp;<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&nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;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>
the value being the attribute to use (without <code>#</code> attribute prefix).</p>
<aside
class="admonition note">
<p>It's currently not possible to set a relation as the grouping criteria.
There are plans to add support for it.</p>
</aside>
<h2>Limitations</h2>
<ul>
<li>It is not possible yet to use group by a relation, only by label.</li>
</ul>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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>
</ul>
<p>For a complete list of configuration environment variables (network settings,
authentication, sync, etc.), see&nbsp;<a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a>.</p>
authentication, sync, etc.), see <a class="reference-link" href="#root/_help_dmi3wz9muS2O">Configuration (config.ini or environment variables)</a>.</p>
<h3>Volume Permissions</h3>
<p>If you encounter permission issues with the data volume, ensure that:</p>
<ol>

View File

@@ -1,48 +0,0 @@
<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&nbsp;<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>

View File

@@ -256,9 +256,7 @@
"multi-factor-authentication-title": "Autentificare multi-factor",
"ai-llm-title": "AI/LLM",
"localization": "Limbă și regiune",
"inbox-title": "Inbox",
"command-palette": "Deschide paleta de comenzi",
"zen-mode": "Mod zen"
"inbox-title": "Inbox"
},
"notes": {
"new-note": "Notiță nouă",
@@ -276,8 +274,7 @@
"export_filter": "Document PDF (*.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-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"
"unable-to-save-message": "Nu s-a putut scrie fișierul selectat. Încercați din nou sau selectați altă destinație."
},
"tray": {
"bookmarks": "Semne de carte",
@@ -430,8 +427,7 @@
"presentation": "Prezentare",
"presentation_slide": "Slide de prezentare",
"presentation_slide_first": "Primul slide",
"presentation_slide_second": "Al doilea slide",
"background": "Fundal"
"presentation_slide_second": "Al doilea slide"
},
"sql_init": {
"db_not_initialized_desktop": "Baza de date nu este inițializată, urmați instrucțiunile de pe ecran.",

View File

@@ -5,7 +5,6 @@ import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import tree from "./tree.js";
import cls from "./cls.js";
import { buildNote } from "../test/becca_easy_mocking.js";
describe("Tree", () => {
let rootNote!: NoteBuilder;
@@ -74,43 +73,4 @@ describe("Tree", () => {
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" ]);
});
});

View File

@@ -136,8 +136,8 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
const topBEl = fetchValue(b, "top");
if (topAEl !== topBEl) {
if (topAEl === null) return reverse ? -1 : 1;
if (topBEl === null) return reverse ? 1 : -1;
if (topAEl === null) return 1;
if (topBEl === null) return -1;
// since "top" should not be reversible, we'll reverse it once more to nullify this effect
return compare(topAEl, topBEl) * (reverse ? -1 : 1);
@@ -147,8 +147,8 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
const bottomBEl = fetchValue(b, "bottom");
if (bottomAEl !== bottomBEl) {
if (bottomAEl === null) return reverse ? 1 : -1;
if (bottomBEl === null) return reverse ? -1 : 1;
if (bottomAEl === null) return -1;
if (bottomBEl === null) return 1;
// since "bottom" should not be reversible, we'll reverse it once more to nullify this effect
return compare(bottomBEl, bottomAEl) * (reverse ? -1 : 1);

View File

@@ -325,11 +325,6 @@ function renderText(result: Result, note: SNote | BNote) {
// Apply syntax highlight.
for (const codeEl of document.querySelectorAll("pre code")) {
if (codeEl.classList.contains("language-mermaid") && note.type === "text") {
// Mermaid is handled on client-side, we don't want to break it by adding syntax highlighting.
continue;
}
const highlightResult = highlightAuto(codeEl.innerText);
codeEl.innerHTML = highlightResult.value;
codeEl.classList.add("hljs");

View File

@@ -2,19 +2,11 @@
"hero_section": {
"github": "깃허브",
"dockerhub": "도커 허브",
"get_started": "시작하기",
"title": "생각을 정리하고, 개인 지식 기반을 구축하세요.",
"subtitle": "Trilium은 개인 지식 베이스를 정리하고 노트를 작성하는 오픈소스 솔루션입니다. 데스크톱에서 로컬로 사용하거나, 자체 호스팅 서버와 동기화하여 어디에서나 노트를 보관할 수 있습니다.",
"screenshot_alt": "Trilium Notes 데스크톱 애플리케이션의 스크린샷"
"get_started": "시작하기"
},
"get-started": {
"title": "시작하기",
"desktop_title": "데스크탑 애플리케이션 내려받기 (v{{version}})",
"older_releases": "오래된 릴리즈 보기",
"architecture": "아키텍쳐:",
"server_title": "여러 기기에서 액세스할 수 있는 서버 설정"
},
"download_now": {
"text": "지금 내려받기 "
"older_releases": "오래된 릴리즈 보기"
}
}

View File

@@ -51,8 +51,7 @@
"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_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>.",
"title": "Multiple modalități de a reprezenta informația"
"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>."
},
"extensibility_benefits": {
"title": "Partajare și extensibilitate",
@@ -73,10 +72,7 @@
"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.",
"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.",
"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ă."
"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."
},
"faq": {
"title": "Întrebări frecvente",

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.99.5",
"appVersion": "0.99.4",
"files": [
{
"isClone": false,
@@ -110,13 +110,6 @@
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "4nwtTJyjNDKd",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "iconClass",
@@ -124,6 +117,13 @@
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "4nwtTJyjNDKd",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "shareAlias",
@@ -1260,19 +1260,12 @@
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "ccIoz7nqgDRK",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "zdQzavvHDl1k",
"isInheritable": false,
"position": 20
"position": 10
},
{
"type": "label",
@@ -1287,6 +1280,13 @@
"value": "releasing",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "ccIoz7nqgDRK",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
@@ -1961,13 +1961,6 @@
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "lXjOyKpUSKgE",
"isInheritable": false,
"position": 20
},
{
"type": "label",
"name": "iconClass",
@@ -2078,34 +2071,6 @@
"format": "markdown",
"dataFileName": "Server translations.md",
"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": []
}
]
},

View File

@@ -27,7 +27,11 @@ Follow the <a class="reference-link" href="Internationalisation%20%20Translatio
### Adding a new locale
See <a class="reference-link" href="Internationalisation%20%20Translations/Adding%20a%20new%20locale.md">Adding a new locale</a>.
To add a new locale, go to `src/public/translations` with your favorite text editor and copy the `en` directory.
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

View File

@@ -1,11 +0,0 @@
# Adding a new locale
Once the Weblate translations for a single language have reached ~50% in coverage, it's time to add it to the application.
To do so:
1. In `packages/commons` look for `i18n.ts` and add a new entry to `UNSORTED_LOCALES` for the language.
2. In `apps/server` look for `services/i18n.ts` and add a mapping for the new language in `DAYJS_LOADER`. Sort the entire list.
3. In `apps/client`, look for `collections/calendar/index.tsx` and modify `LOCALE_MAPPINGS` to add support to the new language.
4. In `apps/client`, look for `widgets/type_widgets/canvas/i18n.ts` and modify `LANGUAGE_MAPPINGS`. A unit test ensures that the language is actually loadable.
5. In `apps/client`, look for `widgets/type_widgets/MindMap.tsx` and modify `LOCALE_MAPPINGS`. The type definitions should already validate if the new value is supported by Mind Elixir.
6. In `packages/ckeditor5`, look for `i18n.ts` and modify `LOCALE_MAPPINGS`. The import validation should already check if the new value is supported by CKEditor, and there's also a test to ensure it.

View File

@@ -1,5 +1,5 @@
# Documentation
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/eyrnitqBQ2w6/Documentation_image.png" width="205" height="162">
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/E6pFkO6VwPFI/Documentation_image.png" width="205" height="162">
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.

28
docs/README-ko.md vendored
View File

@@ -20,10 +20,10 @@ releases)](https://img.shields.io/github/downloads/triliumnext/trilium/total)\
[![Translation
status](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/)
[영어](./README.md) | [중국어 (간체)](./docs/README-ZH_CN.md) | [중국어
(번체)](./docs/README-ZH_TW.md) | [러시아어](./docs/README-ru.md) |
[일본어](./docs/README-ja.md) | [이탈리아어](./docs/README-it.md) |
[스페인어](./docs/README-es.md)
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) |
[Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md)
| [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) |
[Spanish](./docs/README-es.md)
Trilium Notes는 대규모 개인 지식 기반 구축에 중점을 둔 무료 오픈 소스 크로스 플랫폼 계층적 메모 작성 애플리케이션입니다.
@@ -43,9 +43,10 @@ quick overview:
**[docs.triliumnotes.org](https://docs.triliumnotes.org/)에서 포괄적인 문서를 방문하세요**
저희 문서는 다양한 형식으로 제공됩니다.
- **온라인 문서**: [docs.triliumnotes.org](https://docs.triliumnotes.org/)에서 모든 문서를
보여줍니다
- **도움말**: 트릴리움 어플리케이션에서 `F1` 버튼을 눌러 같은 문서를 직접 볼 수 있습니다
- **Online Documentation**: Browse the full documentation at
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
- **In-App Help**: Press `F1` within Trilium to access the same documentation
directly in the application
- **GitHub**: Navigate through the [User
Guide](./docs/User%20Guide/User%20Guide/) in this repository
@@ -129,7 +130,7 @@ related goodies:
themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
## ❓ TriliumNext일까?
## ❓Why TriliumNext?
The original Trilium developer ([Zadam](https://github.com/zadam)) has
graciously given the Trilium repository to the community project which resides
@@ -165,7 +166,7 @@ features, suggestions, or issues you may have!
## 🏗 Installation
### 윈도우 / 맥OS
### Windows / MacOS
Download the binary release for your platform from the [latest release
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
@@ -193,7 +194,7 @@ interface (which is almost identical to the desktop app).
Currently only the latest versions of Chrome & Firefox are supported (and
tested).
### 모바일
### Mobile
To use TriliumNext on a mobile device, you can use a mobile web browser to
access the mobile interface of a server installation (see below).
@@ -208,7 +209,7 @@ repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to
disable automatic updates on your server installation (see below) when using
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
### 서버
### Server
To install TriliumNext on your own server (including via Docker from
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
@@ -259,9 +260,8 @@ pnpm install
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
```
자세한 내용은 [development
docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide)
참고하세요.
For more details, see the [development
docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
### Developer Documentation

11
docs/README-ro.md vendored
View File

@@ -34,13 +34,12 @@ ecran](https://triliumnext.github.io/Docs/Wiki/screenshot-tour):
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
## ⏬ Descarcă
## ⏬ Download
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest)
versiune stabilă, recomandată pentru majoritatea utilizatorilor.
- [Versiune
periodică](https://github.com/TriliumNext/Trilium/releases/tag/nightly)
versiune pentru dezvoltare și testare, actualizată zilnic și cu ultimele
funcționalități și buguri reparate.
stable version, recommended for most users.
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly)
unstable development version, updated daily with the latest features and
fixes.
## 📚 Documentație

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.99.5",
"appVersion": "0.99.4",
"files": [
{
"isClone": false,

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.99.5",
"appVersion": "0.99.4",
"files": [
{
"isClone": false,
@@ -681,13 +681,6 @@
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "Gzjqa934BdH4",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "shareAlias",
@@ -1051,35 +1044,6 @@
"format": "markdown",
"dataFileName": "Trusted proxy.md",
"attachments": []
},
{
"isClone": false,
"noteId": "5ERVJb9s4FRD",
"notePath": [
"pOsGYCXsbNQG",
"Otzi9La2YAUX",
"WOcw2SLH6tbX",
"vcjrb3VVYPZI",
"5ERVJb9s4FRD"
],
"title": "Traefik",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "shareAlias",
"value": "traefik",
"isInheritable": false,
"position": 30
}
],
"format": "markdown",
"dataFileName": "Traefik.md",
"attachments": []
}
]
},
@@ -9907,41 +9871,6 @@
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "IakOLONlIfGI",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "Cq5X6iKQop6R",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "bdUJEHsAPYQR",
"isInheritable": false,
"position": 60
},
{
"type": "label",
"name": "iconClass",
@@ -9960,29 +9889,13 @@
"format": "markdown",
"dataFileName": "Kanban Board.md",
"attachments": [
{
"attachmentId": "3ze7RpkjIWdW",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Kanban Board_image.png"
},
{
"attachmentId": "IrIeh59VGjHq",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Kanban Board_image.png"
},
{
"attachmentId": "usSSa0WI6dDK",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Kanban Board_image.png"
"dataFileName": "Kanban Board_image.png"
}
]
},
@@ -10004,72 +9917,58 @@
{
"type": "relation",
"name": "internalLink",
"value": "zEY4DaJG4YT5",
"value": "KSZ04uQ2D1St",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "KSZ04uQ2D1St",
"value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "0ESUbbAxVnoK",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "XpOYSgsLkTJy",
"value": "IakOLONlIfGI",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"value": "lgKX7r3aL30x",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "IakOLONlIfGI",
"value": "ZjLYv08Rp3qC",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "lgKX7r3aL30x",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "ZjLYv08Rp3qC",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "BlN9DFI679QC",
"isInheritable": false,
"position": 100
"position": 80
},
{
"type": "label",
@@ -11276,44 +11175,23 @@
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"value": "BlN9DFI679QC",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "eIg8jdvaoNNd",
"value": "OFXdgB2nNk1F",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "BlN9DFI679QC",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "bwZpz2ajCEwO",
"isInheritable": false,
"position": 80
"position": 50
},
{
"type": "label",
@@ -11773,40 +11651,12 @@
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "BlN9DFI679QC",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "bwZpz2ajCEwO",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "GTwFsgaA0lCt",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "zP3PMqaG71Ct",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "R9pX4DGra2Vt",
"isInheritable": false,
"position": 60
"position": 20
},
{
"type": "label",
@@ -11820,43 +11670,27 @@
"name": "iconClass",
"value": "bx bx-table",
"isInheritable": false,
"position": 20
"position": 40
}
],
"format": "markdown",
"dataFileName": "Promoted Attributes.md",
"attachments": [
{
"attachmentId": "8ue55DaAJ82K",
"attachmentId": "4EcBRWF9iCk2",
"title": "image.png",
"role": "image",
"mime": "image/png",
"mime": "image/jpg",
"position": 10,
"dataFileName": "Promoted Attributes_image.png"
},
{
"attachmentId": "bLMPNRtMAaKo",
"title": "image.png",
"attachmentId": "Txf5Jdm2vqt2",
"title": "promoted-attributes.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Promoted Attributes_image.png"
},
{
"attachmentId": "FbNQB8xcY0Nu",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Promoted Attributes_image.png"
},
{
"attachmentId": "yBIe1DyxuL2e",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Promoted Attributes_image.png"
"dataFileName": "Promoted Attributes_promot.png"
}
]
}

View File

@@ -12,30 +12,19 @@ In Trilium, attributes are key-value pairs assigned to notes, providing addition
These attributes play a crucial role in organizing, categorizing, and enhancing the functionality of notes.
## Types of attributes
Conceptually there are two types of attributes (applying to both labels and relations):
1. **System attributes**
As the name suggest, these attributes have a special meaning since they are interpreted by Trilium. For example the `color` attribute will change the color of the note as displayed in the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a> and links, and `iconClass` will change the icon of a note.
2. **User-defined attributes**
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="../Basic%20Concepts%20and%20Features/Navigation/Search.md">Search</a>), or they can be given meaning through the use of <a class="reference-link" href="../Scripting.md">Scripting</a>.
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 `iconClass`). Keep this in mind when creating  <a class="reference-link" href="Attributes/Promoted%20Attributes.md">Promoted Attributes</a> in order not to accidentally alter a system attribute (unless intended).
## Viewing the list of attributes
Both the labels and relations for the current note are displayed in the _Owned Attributes_ section of the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Ribbon.md">Ribbon</a>, where they can be viewed and edited. Inherited attributes are displayed in the _Inherited Attributes_ section of the ribbon, where they can only be viewed.
In the list of attributes, labels are prefixed with the `#` character whereas relations are prefixed with the `~` character.
## Attribute Definitions and Promoted Attributes
<a class="reference-link" href="Attributes/Promoted%20Attributes.md">Promoted Attributes</a> create a form-like editing experience for attributes, which makes it easy to enhancing the organization and management of attributes
## Multiplicity
Attributes in Trilium can be "multi-valued", meaning multiple attributes with the same name can co-exist. This can be combined with <a class="reference-link" href="Attributes/Promoted%20Attributes.md">Promoted Attributes</a> to easily add them.
Attributes in Trilium can be "multi-valued", meaning multiple attributes with the same name can co-exist.
## Attribute Definitions and Promoted Attributes
Special labels create "label/attribute" definitions, enhancing the organization and management of attributes. For more details, see <a class="reference-link" href="Attributes/Promoted%20Attributes.md">Promoted Attributes</a>.
## Attribute Inheritance

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,74 +1,31 @@
# Promoted Attributes
<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>
Promoted attributes are [attributes](../Attributes.md) which are considered important and thus are "promoted" onto the main note UI. See example below:
Promoted attributes are [attributes](../Attributes.md) which are displayed prominently in the UI which allow them to be easily viewed and edited.
![](Promoted%20Attributes_promot.png)
One way of seeing promoted attributes is as a kind of form with several fields. Each field is just regular attribute, the only difference is that they appear on the note itself.
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.
Attributes can be pretty useful since they allow for querying and script 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.
Now, how do we make attribute to appear on the UI?
## Attribute definition
In order to have promoted attributes, there needs to be a way to define them.
Attribute is always name-value pair where both name and value are strings.
<figure class="image image-style-align-right image_resized" style="width:38.82%;"><img style="aspect-ratio:492/346;" src="1_Promoted Attributes_image.png" width="492" height="346"></figure>
_Attribute definition_ specifies how should this value be interpreted - is it just string, or is it a date? Should we allow multiple values or note? And importantly, should we _promote_ the attribute or not?
Technically, attributes are only name-value pairs where both name and value are strings.
![](Promoted%20Attributes_image.png)
The _Attribute definition_ specifies how should this value be interpreted:
You can notice tag attribute definition. These "definition" attributes define how the "value" attributes should behave.
* Is it just string, or is it a date?
* Should we allow multiple values or note?
* Should we _promote_ the attribute or not?
## Creating a new promoted attribute definition
To create a new promoted attribute:
1. Go to a note.
2. Go to _Owned Attributes_ in the <a class="reference-link" href="../../Basic%20Concepts%20and%20Features/UI%20Elements/Ribbon.md">Ribbon</a>.
3. Press the + button.
4. Select either _Add new label definition_ or _Add new relation definition_.
5. Select the name which will be name of the label or relation that will be created when the promoted attribute is edited.
6. Ensure _Promoted_ is checked in order to display it at the top of notes.
7. Optionally, choose an _Alias_ 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.
8. Check _Inheritable_ to apply it to this note and all its descendants. To keep it only for the current note, un-check it.
9. Press “Save & Close” to apply the changes.
## How attribute definitions actually work
When a new promoted attribute definition is created, it creates a corresponding label prefixed with either `label` or `relation`, depending on the definition type:
```
#label:myColor(inheritable)="promoted,alias=Color,multi,color"
```
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.
| | |
| --- | --- |
| <figure class="image"><img style="aspect-ratio:495/157;" src="2_Promoted Attributes_image.png" width="495" height="157"></figure> | Notice how the promoted attribute definition only creates a “Due date” box above the text content. |
| <figure class="image"><img style="aspect-ratio:663/160;" src="3_Promoted Attributes_image.png" width="663" height="160"></figure> | 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. |
So there's one attribute for value and one for definition. But notice how an definition attribute can be made [Inheritable](Attribute%20Inheritance.md), meaning that it's also applied to all descendant notes. In this case, the definition used for the whole sub-tree while "value" attributes are for each not individually.
## Using system attributes
It's possible to create promoted attributes out of system attributes, to be able to easily alter them.
Here are a few practical examples:
* <a class="reference-link" href="../../Collections.md">Collections</a> already make use of this practice, for example:
* Calendars add “Start Date”, “End Date”, “Start Time” and “End Time” as promoted attributes. These map to system attributes such as `startDate` which are then interpreted by the calendar view.
* <a class="reference-link" href="../../Collections/Presentation.md">Presentation</a> adds a “Background” promoted attribute for each of the slide to easily be able to customize.
* The Trilium documentation (which is edited in Trilium) uses a promoted attribute to be able to easily edit the `#shareAlias` (see <a class="reference-link" href="../Sharing.md">Sharing</a>) in order to form clean URLs.
* If you always edit a particular system attribute such as `#color`, simply create a promoted attribute for it to make it easier.
So there's one attribute for value and one for definition. But notice how definition attribute is [Inheritable](Attribute%20Inheritance.md), meaning that it's also applied to all descendant note. So in a way, this definition is used for the whole subtree while "value" attributes are applied only for this note.
### Inverse relation
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 there might be a relation between those two of `isPartnerOf`. This is bidirectional relationship - meaning that if a relation is pointing from husband to wife then there should be always another relation pointing from wife to husband.
Another example is with parent-child relationship. Again these always occur in pairs, but in this case it's not exact same relation - the one going from parent to child might be called `isParentOf` and the other one going from child to parent might be called `isChildOf`.
Another example is with parent - child relationship. Again these always occur in pairs, but in this case it's not exact same relation - the one going from parent to child might be called `isParentOf` and the other one going from child to parent might be called `isChildOf`.
Relation definition allows you to specify such "inverse relation" - for the relation you just define you specify which is the inverse relation. Note that in the second example we should have two relation definitions - one for `isParentOf` which defines `isChildOf` as inverse relation and then second relation definition for `isChildOf` which defines `isParentOf` as inverse relation.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -48,7 +48,7 @@ In the _Collections_ tab in the <a class="reference-link" href="../Basic%20Conc
The following attributes can be added to the Collection type:
<table><thead><tr><th>Name</th><th>Description</th></tr></thead><tbody><tr><td><code>#calendar:hideWeekends</code></td><td>When present (regardless of value), it will hide Saturday and Sundays from the calendar.</td></tr><tr><td><code>#calendar:weekNumbers</code></td><td>When present (regardless of value), it will show the number of the week on the calendar.</td></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><td><code>#calendar:view</code></td><td><p>Which view to display in the calendar:</p><ul><li><code>timeGridWeek</code> for the <em>week</em> view;</li><li><code>dayGridMonth</code> for the <em>month</em> view;</li><li><code>multiMonthYear</code> for the <em>year</em> view;</li><li><code>listMonth</code> for the <em>list</em> view.</li></ul><p>Any other value will be dismissed and the default view (month) will be used instead.</p><p>The value of this label is automatically updated when changing the view using the UI buttons.</p></td></tr><tr><td><code>~child:template</code></td><td>Defines the template for newly created notes in the calendar (via dragging or clicking).</td></tr></tbody></table>
<table><thead><tr><th>Name</th><th>Description</th></tr></thead><tbody><tr><td><code>#calendar:hideWeekends</code></td><td>When present (regardless of value), it will hide Saturday and Sundays from the calendar.</td></tr><tr><td><code>#calendar:weekNumbers</code></td><td>When present (regardless of value), it will show the number of the week on the calendar.</td></tr><tr><td><code>#calendar:view</code></td><td><p>Which view to display in the calendar:</p><ul><li><code>timeGridWeek</code> for the <em>week</em> view;</li><li><code>dayGridMonth</code> for the <em>month</em> view;</li><li><code>multiMonthYear</code> for the <em>year</em> view;</li><li><code>listMonth</code> for the <em>list</em> view.</li></ul><p>Any other value will be dismissed and the default view (month) will be used instead.</p><p>The value of this label is automatically updated when changing the view using the UI buttons.</p></td></tr><tr><td><code>~child:template</code></td><td>Defines the template for newly created notes in the calendar (via dragging or clicking).</td></tr></tbody></table>
In addition, the first day of the week can be either Sunday or Monday and can be adjusted from the application settings.

View File

@@ -1,6 +1,6 @@
# Geo Map
> [!IMPORTANT]
> <a class="reference-link" href="../Advanced%20Usage/Attributes.md">Attributes</a><a class="reference-link" href="../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a><a class="reference-link" href="../Advanced%20Usage/Attributes.md">Attributes</a>Starting with Trilium v0.97.0, the geo map has been converted from a standalone [note type](../Note%20Types.md) to a type of view for the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Notes/Note%20List.md">Note List</a>. 
> Starting with Trilium v0.97.0, the geo map has been converted from a standalone [note type](../Note%20Types.md) to a type of view for the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Notes/Note%20List.md">Note List</a>. 
<figure class="image image-style-align-center"><img style="aspect-ratio:892/675;" src="9_Geo Map_image.png" width="892" height="675"></figure>
@@ -26,8 +26,8 @@ The position on the map and the zoom are saved inside the map note and restored
| | | |
| --- | --- | --- |
| 1 | To create a marker, first navigate to the desired point on the map. Then press the ![](10_Geo%20Map_image.png) button in the [Floating buttons](../Basic%20Concepts%20and%20Features/UI%20Elements/Floating%20buttons.md) (top-right) area.     <br> <br>If the button is not visible, make sure the button section is visible by pressing the chevron button (![](17_Geo%20Map_image.png)) in the top-right of the map. | |
| 2 | <img class="image_resized" style="aspect-ratio:1730/416;width:100%;" src="2_Geo Map_image.png" width="1730" height="416"> | Once pressed, the map will enter in the insert mode, as illustrated by the notification.        <br> <br>Simply click the point on the map where to place the marker, or the Escape key to cancel. |
| 1 | To create a marker, first navigate to the desired point on the map. Then press the ![](10_Geo%20Map_image.png) button in the [Floating buttons](../Basic%20Concepts%20and%20Features/UI%20Elements/Floating%20buttons.md) (top-right) area.    <br> <br>If the button is not visible, make sure the button section is visible by pressing the chevron button (![](17_Geo%20Map_image.png)) in the top-right of the map. | |
| 2 | <img class="image_resized" style="aspect-ratio:1730/416;width:100%;" src="2_Geo Map_image.png" width="1730" height="416"> | Once pressed, the map will enter in the insert mode, as illustrated by the notification.       <br> <br>Simply click the point on the map where to place the marker, or the Escape key to cancel. |
| 3 | <img class="image_resized" style="aspect-ratio:1586/404;width:100%;" src="7_Geo Map_image.png" width="1586" height="404"> | Enter the name of the marker/note to be created. |
| 4 | <img class="image_resized" style="aspect-ratio:1696/608;width:100%;" src="16_Geo Map_image.png" width="1696" height="608"> | Once confirmed, the marker will show up on the map and it will also be displayed as a child note of the map. |
@@ -35,7 +35,7 @@ The position on the map and the zoom are saved inside the map note and restored
1. Right click anywhere on the map, where to place the newly created marker (and corresponding note).
2. Select _Add a marker at this location_.
3. Enter the name of the ne<a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a>wly created note.
3. Enter the name of the newly created note.
4. The map should be updated with the new marker.
### Adding an existing note on note from the note tree
@@ -109,7 +109,7 @@ The value of the attribute is made up of the latitude and longitude separated by
| | | |
| --- | --- | --- |
| 1 | <figure class="image image-style-align-center image_resized" style="width:56.84%;"><img style="aspect-ratio:732/918;" src="12_Geo Map_image.png" width="732" height="918"></figure> | Go to Google Maps on the web and look for a desired location, right click on it and a context menu will show up.        <br> <br>Simply click on the first item displaying the coordinates and they will be copied to clipboard.        <br> <br>Then paste the value inside the text box into the `#geolocation` attribute of a child note of the map (don't forget to surround the value with a `"` character). |
| 1 | <figure class="image image-style-align-center image_resized" style="width:56.84%;"><img style="aspect-ratio:732/918;" src="12_Geo Map_image.png" width="732" height="918"></figure> | Go to Google Maps on the web and look for a desired location, right click on it and a context menu will show up.       <br> <br>Simply click on the first item displaying the coordinates and they will be copied to clipboard.       <br> <br>Then paste the value inside the text box into the `#geolocation` attribute of a child note of the map (don't forget to surround the value with a `"` character). |
| 2 | <figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:518/84;" src="4_Geo Map_image.png" width="518" height="84"></figure> | In Trilium, create a child note under the map. |
| 3 | <figure class="image image-style-align-center image_resized" style="width:100%;"><img style="aspect-ratio:1074/276;" src="11_Geo Map_image.png" width="1074" height="276"></figure> | And then go to Owned Attributes and type `#geolocation="`, then paste from the clipboard as-is and then add the ending `"` character. Press Enter to confirm and the map should now be updated to contain the new note. |
@@ -120,7 +120,7 @@ Similarly to the Google Maps approach:
| | | |
| --- | --- | --- |
| 1 | <img class="image_resized" style="aspect-ratio:562/454;width:100%;" src="1_Geo Map_image.png" width="562" height="454"> | Go to any location on openstreetmap.org and right click to bring up the context menu. Select the “Show address” item. |
| 2 | <img class="image_resized" style="aspect-ratio:696/480;width:100%;" src="Geo Map_image.png" width="696" height="480"> | The address will be visible in the top-left of the screen, in the place of the search bar.        <br> <br>Select the coordinates and copy them into the clipboard. |
| 2 | <img class="image_resized" style="aspect-ratio:696/480;width:100%;" src="Geo Map_image.png" width="696" height="480"> | The address will be visible in the top-left of the screen, in the place of the search bar.       <br> <br>Select the coordinates and copy them into the clipboard. |
| 3 | <img class="image_resized" style="aspect-ratio:640/276;width:100%;" src="5_Geo Map_image.png" width="640" height="276"> | Simply paste the value inside the text box into the `#geolocation` attribute of a child note of the map and then it should be displayed on the map. |
## Adding GPS tracks (.gpx)
@@ -131,7 +131,7 @@ Trilium has basic support for displaying GPS tracks on the geo map.
| --- | --- | --- |
| 1 | <figure class="image image-style-align-center"><img style="aspect-ratio:226/74;" src="3_Geo Map_image.png" width="226" height="74"></figure> | To add a track, simply drag & drop a .gpx file inside the geo map in the note tree. |
| 2 | <figure class="image image-style-align-center"><img style="aspect-ratio:322/222;" src="14_Geo Map_image.png" width="322" height="222"></figure> | In order for the file to be recognized as a GPS track, it needs to show up as `application/gpx+xml` in the _File type_ field. |
| 3 | <figure class="image image-style-align-center"><img style="aspect-ratio:620/530;" src="6_Geo Map_image.png" width="620" height="530"></figure> | When going back to the map, the track should now be visible.        <br> <br>The start and end points of the track are indicated by the two blue markers. |
| 3 | <figure class="image image-style-align-center"><img style="aspect-ratio:620/530;" src="6_Geo Map_image.png" width="620" height="530"></figure> | When going back to the map, the track should now be visible.       <br> <br>The start and end points of the track are indicated by the two blue markers. |
> [!NOTE]
> The starting point of the track will be displayed as a marker, with the name of the note underneath. The start marker will also respect the icon and the `color` of the note. The end marker is displayed with a distinct icon.

View File

@@ -1,5 +1,5 @@
# Kanban Board
<figure class="image"><img style="aspect-ratio:918/248;" src="2_Kanban Board_image.png" width="918" height="248"></figure>
<figure class="image"><img style="aspect-ratio:918/248;" src="Kanban Board_image.png" width="918" height="248"></figure>
The Board view presents sub-notes in columns for a Kanban-like experience. Each column represents a possible value for a status label, which can be adjusted.
@@ -41,20 +41,7 @@ Notes are displayed recursively, so even the child notes of the child notes will
* Delete the current note.
* If there are many notes within the column, move the mouse over the column and use the mouse wheel to scroll.
### Working with the note tree
It's also possible to add items on the board using the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a>.
1. Select the desired note in the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a>.
2. Hold the mouse on the note and drag it to the to the desired column.
This works for:
* Notes that are not children of the board, case in which a [clone](../Basic%20Concepts%20and%20Features/Notes/Cloning%20Notes.md) will be created.
* Notes that are children of the board, but not yet assigned on the board.
* Notes that are children of the board, case in which they will be moved to the new column.
### Keyboard interaction
## Keyboard interaction
The board view has mild support for keyboard-based navigation:
@@ -65,60 +52,13 @@ The board view has mild support for keyboard-based navigation:
## Configuration
### Displaying custom attributes
### Grouping by another attribute
<figure class="image image-style-align-center"><img style="aspect-ratio:531/485;" src="Kanban Board_image.png" width="531" height="485"></figure>
By default, the label used to group the notes is `#status`. It is possible to use a different label if needed by defining a label named `#board:groupBy` with the value being the attribute to use (without `#` attribute prefix).
Note attributes can be displayed on the board to enhance it with custom information such as adding a Due date for your tasks.
> [!NOTE]
> It's currently not possible to set a relation as the grouping criteria. There are plans to add support for it.
This feature works exclusively via attribute definitions (<a class="reference-link" href="../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a>). The easiest way to add these is:
## Limitations
1. Go to board note.
2. In the ribbon select _Owned Attributes_ → plus button → _Add new label/relation definition_.
3. Configure the attribute as desired.
4. Check _Inheritable_ to make it applicable to child notes automatically.
After creating the attribute, click on a note and fill in the promoted attributes which should then reflect inside the board.
Of note:
* 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.
* Both “Single value” and “Multi value” attributes are supported. In case of multi-value, a badge is displayed for every instance of the attribute.
* All label types are supported, including dates, booleans and URLs.
* Relation attributes are also supported as well, showing a link with the target note title and icon.
* Currently, it's not possible to adjust which promoted attributes are displayed, since all promoted attributes will be displayed (except the `board:groupBy` one). There are plans to improve upon this being able to hide promoted attributes individually.
### Grouping by another label
By default, the label used to group the notes is `#status`. It is possible to use a different label if needed by defining a label named `#board:groupBy` with the value being the attribute to use (with or without `#` attribute prefix).
### Grouping by relations
<figure class="image image-style-align-right"><img style="aspect-ratio:535/245;" src="1_Kanban Board_image.png" width="535" height="245"></figure>
A more advanced use-case is grouping by [Relations](../Advanced%20Usage/Attributes/Relations.md).
During this mode:
* The columns represent the _target notes_ of a relation.
* When creating a new column, a note is selected instead of a column name.
* The column icon will match the target note.
* Moving notes between columns will change its relation.
* Renaming an existing column will change the target note of all the notes in that column.
Using relations instead of labels has some benefits:
* The status/grouping of the notes is visible outside the Kanban board, for example on the <a class="reference-link" href="../Note%20Types/Note%20Map.md">Note Map</a>.
* Columns can have icons.
* 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.
To do so:
1. First, create a Kanban board from scratch and not a template:
2. Assign `#viewType=board #hidePromotedAttributes` to emulate the default template.
3. Set `#board:groupBy` to the name of a relation to group by, **including the** `**~**` **prefix** (e.g. `~status`).
4. Optionally, use <a class="reference-link" href="../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a> for easy status change within the note:
```
#relation:status(inheritable)="promoted,alias=Status,single"
```
* It is not possible yet to use group by a relation, only by label.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -187,7 +187,7 @@ docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-
* `TRILIUM_GID`: GID to use for the container process (passed to Docker's `--user` flag)
* `TRILIUM_DATA_DIR`: Path to the data directory inside the container (default: `/home/node/trilium-data`)
For a complete list of configuration environment variables (network settings, authentication, sync, etc.), see <a class="reference-link" href="../../../Advanced%20Usage/Configuration%20(config.ini%20or%20e.md">Configuration (config.ini or environment variables)</a>.
For a complete list of configuration environment variables (network settings, authentication, sync, etc.), see <a class="reference-link" href="#root/dmi3wz9muS2O">Configuration (config.ini or environment variables)</a>.
### Volume Permissions

View File

@@ -1,60 +0,0 @@
# Traefik
Configure Traefik proxy and HTTPS. See [#7768](https://github.com/TriliumNext/Trilium/issues/7768#issuecomment-3539165814) for reference
### Build the docker-compose file
Setting up Traefik as reverse proxy requires setting the following labels:
```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
```
### Setup needed environment variables
After setting up a reverse proxy, make sure to configure the <a class="reference-link" href="Trusted%20proxy.md">[missing note]</a>.
### Example `docker-compose.yaml`
```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
```

View File

@@ -23,7 +23,7 @@
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "43.1.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@ckeditor/ckeditor5-package-tools": "5.0.0",
"@typescript-eslint/eslint-plugin": "~8.46.0",
"@typescript-eslint/parser": "8.46.4",
"@vitest/browser": "3.2.4",

View File

@@ -24,7 +24,7 @@
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "43.1.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@ckeditor/ckeditor5-package-tools": "5.0.0",
"@typescript-eslint/eslint-plugin": "~8.46.0",
"@typescript-eslint/parser": "8.46.4",
"@vitest/browser": "3.2.4",

View File

@@ -26,7 +26,7 @@
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "43.1.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@ckeditor/ckeditor5-package-tools": "5.0.0",
"@typescript-eslint/eslint-plugin": "~8.46.0",
"@typescript-eslint/parser": "8.46.4",
"@vitest/browser": "3.2.4",

View File

@@ -27,7 +27,7 @@
"@ckeditor/ckeditor5-dev-build-tools": "43.1.0",
"@ckeditor/ckeditor5-dev-utils": "43.1.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@ckeditor/ckeditor5-package-tools": "5.0.0",
"@typescript-eslint/eslint-plugin": "~8.46.0",
"@typescript-eslint/parser": "8.46.4",
"@vitest/browser": "3.2.4",

View File

@@ -26,7 +26,7 @@
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "43.1.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@ckeditor/ckeditor5-package-tools": "5.0.0",
"@typescript-eslint/eslint-plugin": "~8.46.0",
"@typescript-eslint/parser": "8.46.4",
"@vitest/browser": "3.2.4",

View File

@@ -6,7 +6,6 @@
"type": "module",
"main": "./src/index.ts",
"dependencies": {
"@triliumnext/commons": "workspace:*",
"@triliumnext/ckeditor5-admonition": "workspace:*",
"@triliumnext/ckeditor5-footnotes": "workspace:*",
"@triliumnext/ckeditor5-keyboard-marker": "workspace:*",
@@ -16,7 +15,7 @@
"ckeditor5-premium-features": "47.2.0"
},
"devDependencies": {
"@smithy/middleware-retry": "4.4.11",
"@smithy/middleware-retry": "4.4.10",
"@types/jquery": "3.5.33"
}
}

View File

@@ -1,90 +0,0 @@
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { EditorConfig, Translations } from "ckeditor5";
interface LocaleMapping {
languageCode: string;
coreTranslation: () => Promise<{ default: Translations }>;
premiumFeaturesTranslation: () => Promise<{ default: Translations }>;
}
const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
en: null,
en_rtl: null,
ar: {
languageCode: "ar",
coreTranslation: () => import("ckeditor5/translations/ar.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ar.js"),
},
cn: {
languageCode: "zh",
coreTranslation: () => import("ckeditor5/translations/zh-cn.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/zh-cn.js"),
},
de: {
languageCode: "de",
coreTranslation: () => import("ckeditor5/translations/de.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/de.js"),
},
es: {
languageCode: "es",
coreTranslation: () => import("ckeditor5/translations/es.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/es.js"),
},
fr: {
languageCode: "fr",
coreTranslation: () => import("ckeditor5/translations/fr.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/fr.js"),
},
it: {
languageCode: "it",
coreTranslation: () => import("ckeditor5/translations/it.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/it.js"),
},
ja: {
languageCode: "ja",
coreTranslation: () => import("ckeditor5/translations/ja.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"),
},
pt: {
languageCode: "pt",
coreTranslation: () => import("ckeditor5/translations/pt.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pt.js"),
},
pt_br: {
languageCode: "pt-br",
coreTranslation: () => import("ckeditor5/translations/pt-br.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pt-br.js"),
},
ro: {
languageCode: "ro",
coreTranslation: () => import("ckeditor5/translations/ro.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ro.js"),
},
tw: {
languageCode: "zh-tw",
coreTranslation: () => import("ckeditor5/translations/zh.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/zh.js"),
},
uk: {
languageCode: "uk",
coreTranslation: () => import("ckeditor5/translations/uk.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/uk.js"),
},
ru: {
languageCode: "ru",
coreTranslation: () => import("ckeditor5/translations/ru.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ru.js")
},
};
export default async function getCkLocale(locale: DISPLAYABLE_LOCALE_IDS): Promise<Pick<EditorConfig, "language" | "translations">> {
const mapping = LOCALE_MAPPINGS[locale];
if (!mapping) return {};
const coreTranslation = (await (mapping.coreTranslation())).default;
const premiumFeaturesTranslation = (await (mapping.premiumFeaturesTranslation())).default;
return {
language: mapping.languageCode,
translations: [ coreTranslation, premiumFeaturesTranslation ]
};
}

View File

@@ -8,7 +8,6 @@ export { PREMIUM_PLUGINS } from "./plugins.js";
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig, WatchdogState } from "ckeditor5";
export type { TemplateDefinition } from "ckeditor5-premium-features";
export { default as buildExtraCommands } from "./extra_slash_commands.js";
export { default as getCkLocale } from "./i18n.js";
// Import with sideffects to ensure that type augmentations are present.
import "@triliumnext/ckeditor5-math";

Some files were not shown because too many files have changed in this diff Show More