Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements

This commit is contained in:
Adorian Doran 2025-10-23 19:43:17 +03:00
commit 40b08e1828
10 changed files with 70 additions and 23 deletions

View File

@ -2432,4 +2432,8 @@ iframe.print-iframe {
bottom: 0; bottom: 0;
width: 0; width: 0;
height: 0; height: 0;
}
.excalidraw.theme--dark canvas {
--theme-filter: invert(100%) hue-rotate(180deg);
} }

View File

@ -47,8 +47,9 @@ export default class RightDropdownButtonWidget extends BasicWidget {
} }
}); });
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title); this.$widget.attr("title", this.title);
this.tooltip = new Tooltip(this.$tooltip[0], { this.tooltip = Tooltip.getOrCreateInstance(this.$widget[0], {
trigger: "hover",
placement: handleRightToLeftPlacement(this.settings.titlePlacement), placement: handleRightToLeftPlacement(this.settings.titlePlacement),
fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ] fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ]
}); });
@ -56,9 +57,7 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.$widget this.$widget
.find(".right-dropdown-button") .find(".right-dropdown-button")
.addClass(this.iconClass) .addClass(this.iconClass)
.on("click", () => this.tooltip.hide()) .on("click", () => this.tooltip.hide());
.on("mouseenter", () => this.tooltip.show())
.on("mouseleave", () => this.tooltip.hide());
this.$widget.on("show.bs.dropdown", async () => { this.$widget.on("show.bs.dropdown", async () => {
await this.dropdownShown(); await this.dropdownShown();

View File

@ -141,7 +141,11 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note:
}) })
.then(({ $renderedContent, type }) => { .then(({ $renderedContent, type }) => {
if (!contentRef.current) return; if (!contentRef.current) return;
contentRef.current.replaceChildren(...$renderedContent); if ($renderedContent[0].innerHTML) {
contentRef.current.replaceChildren(...$renderedContent);
} else {
contentRef.current.replaceChildren();
}
contentRef.current.classList.add(`type-${type}`); contentRef.current.classList.add(`type-${type}`);
highlightSearch(contentRef.current); highlightSearch(contentRef.current);
}) })

View File

@ -46,7 +46,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type); const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
const isInOptions = note.noteId.startsWith("_options"); const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation"); const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation");
const isElectron = getIsElectron(); const isElectron = getIsElectron();
const isMac = getIsMac(); const isMac = getIsMac();
@ -69,10 +69,10 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
<FormDropdownDivider /> <FormDropdownDivider />
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")} <CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptions || note.type === "search"} disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
<CommandItem icon="bx bx-export" text={t("note_actions.export_note")} <CommandItem icon="bx bx-export" text={t("note_actions.export_note")}
disabled={isInOptions || note.noteId === "_backendLog"} disabled={isInOptionsOrHelp || note.noteId === "_backendLog"}
command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { command={() => noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", {
notePath: noteContext.notePath, notePath: noteContext.notePath,
defaultType: "single" defaultType: "single"
@ -84,14 +84,14 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} /> <CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
<FormDropdownDivider /> <FormDropdownDivider />
<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptions} text={t("note_actions.save_revision")} /> <CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
<CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive <CommandItem icon="bx bx-trash destructive-action-icon" text={t("note_actions.delete_note")} destructive
disabled={isInOptions} disabled={isInOptionsOrHelp}
command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])} command={() => branches.deleteNotes([note.getParentBranches()[0].branchId])}
/> />
<FormDropdownDivider /> <FormDropdownDivider />
<CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptions} text={t("note_actions.note_attachments")} /> <CommandItem command="showAttachments" icon="bx bx-paperclip" disabled={isInOptionsOrHelp} text={t("note_actions.note_attachments")} />
</Dropdown> </Dropdown>
); );
} }

View File

@ -152,14 +152,14 @@ function restoreRevision(req: Request) {
} }
function getEditedNotesOnDate(req: Request) { function getEditedNotesOnDate(req: Request) {
const noteIds = sql.getColumn<string>( const noteIds = sql.getColumn<string>(/*sql*/`\
`
SELECT notes.* SELECT notes.*
FROM notes FROM notes
WHERE noteId IN ( WHERE noteId IN (
SELECT noteId FROM notes SELECT noteId FROM notes
WHERE notes.dateCreated LIKE :date WHERE
OR notes.dateModified LIKE :date (notes.dateCreated LIKE :date OR notes.dateModified LIKE :date)
AND (noteId NOT LIKE '_%')
UNION ALL UNION ALL
SELECT noteId FROM revisions SELECT noteId FROM revisions
WHERE revisions.dateLastEdited LIKE :date WHERE revisions.dateLastEdited LIKE :date

View File

@ -681,3 +681,34 @@ describe("#normalizeCustomHandlerPattern", () => {
}); });
}); });
}); });
describe("#slugify", () => {
it("should return a slugified string", () => {
const testString = "This is a Test String! With unicode & Special #Chars.";
const expectedSlug = "this-is-a-test-string-with-unicode-special-chars";
const result = utils.slugify(testString);
expect(result).toBe(expectedSlug);
});
it("supports CJK characters without alteration", () => {
const testString = "测试中文字符";
const expectedSlug = "测试中文字符";
const result = utils.slugify(testString);
expect(result).toBe(expectedSlug);
});
it("supports Cyrillic characters without alteration", () => {
const testString = "Тестирование кириллических символов";
const expectedSlug = "тестирование-кириллических-символов";
const result = utils.slugify(testString);
expect(result).toBe(expectedSlug);
});
// preserves diacritic marks
it("preserves diacritic marks", () => {
const testString = "Café naïve façade jalapeño";
const expectedSlug = "café-naïve-façade-jalapeño";
const result = utils.slugify(testString);
expect(result).toBe(expectedSlug);
});
});

View File

@ -497,6 +497,14 @@ export function formatSize(size: number | null | undefined) {
} }
} }
function slugify(text: string) {
return text
.normalize("NFC") // keep composed form, preserves accents
.toLowerCase()
.replace(/[^\p{Letter}\p{Number}]+/gu, "-") // replace non-letter/number with "-"
.replace(/(^-|-$)+/g, ""); // trim dashes
}
export default { export default {
compareVersions, compareVersions,
crash, crash,
@ -532,6 +540,7 @@ export default {
safeExtractMessageAndStackFromError, safeExtractMessageAndStackFromError,
sanitizeSqlIdentifier, sanitizeSqlIdentifier,
stripTags, stripTags,
slugify,
timeLimit, timeLimit,
toBase64, toBase64,
toMap, toMap,

View File

@ -175,7 +175,8 @@ function register(router: Router) {
appPath: isDev ? appPath : `../${appPath}`, appPath: isDev ? appPath : `../${appPath}`,
showLoginInShareTheme, showLoginInShareTheme,
t, t,
isDev isDev,
utils
}; };
let useDefaultView = true; let useDefaultView = true;

View File

@ -90,9 +90,9 @@ const currentTheme = note.getLabel("shareTheme") === "light" ? "light" : "dark";
const themeClass = currentTheme === "light" ? " theme-light" : " theme-dark"; const themeClass = currentTheme === "light" ? " theme-light" : " theme-dark";
const headingRe = /(<h[1-6]>)(.+?)(<\/h[1-6]>)/g; const headingRe = /(<h[1-6]>)(.+?)(<\/h[1-6]>)/g;
const headingMatches = [...content.matchAll(headingRe)]; const headingMatches = [...content.matchAll(headingRe)];
const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-");
content = content.replaceAll(headingRe, (...match) => { content = content.replaceAll(headingRe, (...match) => {
match[0] = match[0].replace(match[3], `<a id="${slugify(match[2])}" class="toc-anchor" name="${slugify(match[2])}" href="#${slugify(match[2])}">#</a>${match[3]}`); const slug = utils.slugify(utils.stripTags(match[2]));
match[0] = match[0].replace(match[3], `<a id="${slug}" class="toc-anchor" name="${slug}" href="#${slug}">#</a>${match[3]}`);
return match[0]; return match[0];
}); });
%> %>

View File

@ -1,12 +1,11 @@
<% <%
const slugify = (text) => text.toLowerCase().replace(/[^\w]/g, "-"); const strippedName = utils.stripTags(entry.name);
const slug = slugify(entry.name); const slug = utils.slugify(strippedName);
%> %>
<li> <li>
<a href="#<%= slug %>"> <a href="#<%= slug %>">
<span><%= entry.name %></span> <span><%= strippedName %></span>
</a> </a>
<% if (entry.children.length) { %> <% if (entry.children.length) { %>