diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index 0e9db93020..cdfdb67fe1 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -1,18 +1,19 @@ -import renderService from "./render.js"; +import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; +import WheelZoom from 'vanilla-js-wheel-zoom'; + +import FAttachment from "../entities/fattachment.js"; +import FNote from "../entities/fnote.js"; +import imageContextMenuService from "../menus/image_context_menu.js"; +import { t } from "../services/i18n.js"; +import renderText from "./content_renderer_text.js"; +import renderDoc from "./doc_renderer.js"; +import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; +import openService from "./open.js"; import protectedSessionService from "./protected_session.js"; import protectedSessionHolder from "./protected_session_holder.js"; -import openService from "./open.js"; -import utils from "./utils.js"; -import FNote from "../entities/fnote.js"; -import FAttachment from "../entities/fattachment.js"; -import imageContextMenuService from "../menus/image_context_menu.js"; +import renderService from "./render.js"; import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js"; -import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; -import renderDoc from "./doc_renderer.js"; -import { t } from "../services/i18n.js"; -import WheelZoom from 'vanilla-js-wheel-zoom'; -import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; -import renderText from "./content_renderer_text.js"; +import utils from "./utils.js"; let idCounter = 1; @@ -22,6 +23,12 @@ export interface RenderOptions { imageHasZoom?: boolean; /** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */ noChildrenList?: boolean; + /** If enabled, it will prevent rendering of included notes. */ + noIncludedNotes?: boolean; + /** If enabled, it will include archived notes when rendering children list. */ + includeArchivedNotes?: boolean; + /** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */ + seenNoteIds?: Set; } const CODE_MIME_TYPES = new Set(["application/json"]); @@ -152,7 +159,7 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery") .attr("src", url || "") - .attr("id", "attachment-image-" + idCounter++) + .attr("id", `attachment-image-${ idCounter++}`) .css("max-width", "100%"); $renderedContent.append($img); @@ -231,14 +238,14 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: $downloadButton.on("click", (e) => { e.stopPropagation(); - openService.downloadFileNote(entity.noteId) + openService.downloadFileNote(entity.noteId); }); $openButton.on("click", async (e) => { const iconEl = $openButton.find("> .bx"); iconEl.removeClass("bx bx-link-external"); iconEl.addClass("bx bx-loader spin"); e.stopPropagation(); - await openService.openNoteExternally(entity.noteId, entity.mime) + await openService.openNoteExternally(entity.noteId, entity.mime); iconEl.removeClass("bx bx-loader spin"); iconEl.addClass("bx bx-link-external"); }); @@ -266,7 +273,7 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery try { await loadElkIfNeeded(mermaid, content); - const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content); + const { svg } = await mermaid.mermaidAPI.render(`in-mermaid-graph-${ idCounter++}`, content); $renderedContent.append($(postprocessMermaidSvg(svg))); } catch (e) { diff --git a/apps/client/src/services/content_renderer_text.spec.ts b/apps/client/src/services/content_renderer_text.spec.ts new file mode 100644 index 0000000000..4199965ccb --- /dev/null +++ b/apps/client/src/services/content_renderer_text.spec.ts @@ -0,0 +1,132 @@ +import { trimIndentation } from "@triliumnext/commons"; +import { describe, expect, it } from "vitest"; + +import { buildNote } from "../test/easy-froca"; +import renderText from "./content_renderer_text"; + +describe("Text content renderer", () => { + it("renders included note", async () => { + const contentEl = document.createElement("div"); + const includedNote = buildNote({ + title: "Included note", + content: "

This is the included note.

" + }); + const note = buildNote({ + title: "New note", + content: trimIndentation` +

+ Hi there +

+
+   +
+ ` + }); + await renderText(note, $(contentEl)); + expect(contentEl.querySelectorAll("section.include-note").length).toBe(1); + expect(contentEl.querySelectorAll("section.include-note p").length).toBe(1); + }); + + it("skips rendering included note", async () => { + const contentEl = document.createElement("div"); + const includedNote = buildNote({ + title: "Included note", + content: "

This is the included note.

" + }); + const note = buildNote({ + title: "New note", + content: trimIndentation` +

+ Hi there +

+
+   +
+ ` + }); + await renderText(note, $(contentEl), { noIncludedNotes: true }); + expect(contentEl.querySelectorAll("section.include-note").length).toBe(0); + }); + + it("doesn't enter infinite loop on direct recursion", async () => { + const contentEl = document.createElement("div"); + const note = buildNote({ + title: "New note", + id: "Y7mBwmRjQyb4", + content: trimIndentation` +

+ Hi there +

+
+   +
+
+   +
+ ` + }); + await renderText(note, $(contentEl)); + expect(contentEl.querySelectorAll("section.include-note").length).toBe(0); + }); + + it("doesn't enter infinite loop on indirect recursion", async () => { + const contentEl = document.createElement("div"); + buildNote({ + id: "first", + title: "Included note", + content: trimIndentation`\ +

This is the included note.

+
+   +
+ ` + }); + const note = buildNote({ + id: "second", + title: "New note", + content: trimIndentation` +

+ Hi there +

+
+   +
+ ` + }); + await renderText(note, $(contentEl)); + expect(contentEl.querySelectorAll("section.include-note").length).toBe(1); + }); + + it("renders children list when note is empty", async () => { + const contentEl = document.createElement("div"); + const parentNote = buildNote({ + title: "Parent note", + children: [ + { title: "Child note 1" }, + { title: "Child note 2" } + ] + }); + await renderText(parentNote, $(contentEl)); + const items = contentEl.querySelectorAll("a"); + expect(items.length).toBe(2); + expect(items[0].textContent).toBe("Child note 1"); + expect(items[1].textContent).toBe("Child note 2"); + }); + + it("skips archived notes in children list", async () => { + const contentEl = document.createElement("div"); + const parentNote = buildNote({ + title: "Parent note", + children: [ + { title: "Child note 1" }, + { title: "Child note 2", "#archived": "" }, + { title: "Child note 3" } + ] + }); + await renderText(parentNote, $(contentEl)); + const items = contentEl.querySelectorAll("a"); + expect(items.length).toBe(2); + expect(items[0].textContent).toBe("Child note 1"); + expect(items[1].textContent).toBe("Child note 3"); + }); +}); diff --git a/apps/client/src/services/content_renderer_text.ts b/apps/client/src/services/content_renderer_text.ts index 5b388d64e4..090aa7f03c 100644 --- a/apps/client/src/services/content_renderer_text.ts +++ b/apps/client/src/services/content_renderer_text.ts @@ -15,7 +15,14 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon if (blob && !isHtmlEmpty(blob.content)) { $renderedContent.append($('
').html(blob.content)); - await renderIncludedNotes($renderedContent[0]); + + const seenNoteIds = options.seenNoteIds ?? new Set(); + seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId); + if (!options.noIncludedNotes) { + await renderIncludedNotes($renderedContent[0], seenNoteIds); + } else { + $renderedContent.find("section.include-note").remove(); + } if ($renderedContent.find("span.math-tex").length > 0) { renderMathInElement($renderedContent[0], { trust: true }); @@ -35,11 +42,11 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement); await formatCodeBlocks($renderedContent); } else if (note instanceof FNote && !options.noChildrenList) { - await renderChildrenList($renderedContent, note); + await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false); } } -async function renderIncludedNotes(contentEl: HTMLElement) { +async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set) { // TODO: Consider duplicating with server's share/content_renderer.ts. const includeNoteEls = contentEl.querySelectorAll("section.include-note"); @@ -66,8 +73,18 @@ async function renderIncludedNotes(contentEl: HTMLElement) { continue; } - const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent; + if (seenNoteIds.has(noteId)) { + console.warn(`Skipping inclusion of ${noteId} to avoid circular reference.`); + includeNoteEl.remove(); + continue; + } + + const renderedContent = (await content_renderer.getRenderedContent(note, { + seenNoteIds + })).$renderedContent; includeNoteEl.replaceChildren(...renderedContent); + + seenNoteIds.add(noteId); } } @@ -98,7 +115,7 @@ export async function applyInlineMermaid(container: HTMLDivElement) { } } -async function renderChildrenList($renderedContent: JQuery, note: FNote) { +async function renderChildrenList($renderedContent: JQuery, note: FNote, includeArchivedNotes: boolean) { let childNoteIds = note.getChildNoteIds(); if (!childNoteIds.length) { @@ -108,14 +125,16 @@ async function renderChildrenList($renderedContent: JQuery, note: F $renderedContent.css("padding", "10px"); $renderedContent.addClass("text-with-ellipsis"); + // just load the first 10 child notes if (childNoteIds.length > 10) { childNoteIds = childNoteIds.slice(0, 10); } - // just load the first 10 child notes const childNotes = await froca.getNotes(childNoteIds); for (const childNote of childNotes) { + if (childNote.isArchived && !includeArchivedNotes) continue; + $renderedContent.append( await link.createLink(`${note.noteId}/${childNote.noteId}`, { showTooltip: false, diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index c2f2120443..ef9d68c992 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -215,7 +215,7 @@ export default function NoteDetail() { return (
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => { return ) { const noteIds = useFilteredNoteIds(note, unfilteredNoteIds); const { pageNotes, ...pagination } = usePagination(note, noteIds); + const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived"); return (
@@ -53,7 +54,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens } @@ -95,14 +96,14 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan {isExpanded && <> - + }
); } -function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) { +function GridNoteCard({ note, parentNote, highlightedTokens, includeArchived }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined, includeArchived: boolean }) { const titleRef = useRef(null); const [ noteTitle, setNoteTitle ] = useState(); const notePath = getNotePath(parentNote, note); @@ -130,6 +131,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa note={note} trim highlightedTokens={highlightedTokens} + includeArchivedNotes={includeArchived} />
); @@ -146,14 +148,22 @@ function NoteAttributes({ note }: { note: FNote }) { return ; } -function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note: FNote, trim?: boolean, noChildrenList?: boolean, highlightedTokens: string[] | null | undefined }) { +function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: { + note: FNote; + trim?: boolean; + noChildrenList?: boolean; + highlightedTokens: string[] | null | undefined; + includeArchivedNotes: boolean; +}) { const contentRef = useRef(null); const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens); useEffect(() => { content_renderer.getRenderedContent(note, { trim, - noChildrenList + noChildrenList, + noIncludedNotes: true, + includeArchivedNotes }) .then(({ $renderedContent, type }) => { if (!contentRef.current) return; diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx index 887568d379..e8e73ae785 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.tsx +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -88,6 +88,7 @@ export default function PopupEditor() { onHidden={() => setShown(false)} keepInDom // needed for faster loading noFocus // automatic focus breaks block popup + stackable > {!isNewLayout && } diff --git a/apps/client/src/widgets/launch_bar/LauncherContainer.tsx b/apps/client/src/widgets/launch_bar/LauncherContainer.tsx index 26a502a8a2..3450a4c013 100644 --- a/apps/client/src/widgets/launch_bar/LauncherContainer.tsx +++ b/apps/client/src/widgets/launch_bar/LauncherContainer.tsx @@ -1,17 +1,18 @@ import { useCallback, useLayoutEffect, useState } from "preact/hooks"; + import FNote from "../../entities/fnote"; import froca from "../../services/froca"; import { isDesktop, isMobile } from "../../services/utils"; -import CalendarWidget from "./CalendarWidget"; -import SpacerWidget from "./SpacerWidget"; -import BookmarkButtons from "./BookmarkButtons"; -import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget"; -import SyncStatus from "./SyncStatus"; -import HistoryNavigationButton from "./HistoryNavigation"; -import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions"; import { useTriliumEvent } from "../react/hooks"; import { onWheelHorizontalScroll } from "../widget_utils"; +import BookmarkButtons from "./BookmarkButtons"; +import CalendarWidget from "./CalendarWidget"; +import HistoryNavigationButton from "./HistoryNavigation"; import { LaunchBarContext } from "./launch_bar_widgets"; +import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions"; +import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget"; +import SpacerWidget from "./SpacerWidget"; +import SyncStatus from "./SyncStatus"; export default function LauncherContainer({ isHorizontalLayout }: { isHorizontalLayout: boolean }) { const childNotes = useLauncherChildNotes(); @@ -34,18 +35,19 @@ export default function LauncherContainer({ isHorizontalLayout }: { isHorizontal }}> {childNotes?.map(childNote => { if (childNote.type !== "launcher") { - throw new Error(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`); + console.warn(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`); + return false; } if (!isDesktop() && childNote.isLabelTruthy("desktopOnly")) { return false; } - return + return ; })}
- ) + ); } function Launcher({ note, isHorizontalLayout }: { note: FNote, isHorizontalLayout: boolean }) { @@ -72,7 +74,7 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) { const builtinWidget = note.getLabelValue("builtinWidget"); switch (builtinWidget) { case "calendar": - return + return ; case "spacer": // || has to be inside since 0 is a valid value const baseSize = parseInt(note.getLabelValue("baseSize") || "40"); @@ -86,15 +88,15 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) { case "syncStatus": return ; case "backInHistoryButton": - return + return ; case "forwardInHistoryButton": - return + return ; case "todayInJournal": - return + return ; case "quickSearch": - return + return ; case "aiChatLauncher": - return + return ; default: throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); } diff --git a/apps/client/src/widgets/sql_result.tsx b/apps/client/src/widgets/sql_result.tsx index e4fde650b9..7aaa5739d3 100644 --- a/apps/client/src/widgets/sql_result.tsx +++ b/apps/client/src/widgets/sql_result.tsx @@ -23,7 +23,7 @@ export default function SqlResults() { {t("sql_result.no_rows")} ) : ( -
+
{results?.map(rows => { // inserts, updates if (typeof rows === "object" && !Array.isArray(rows)) { diff --git a/apps/client/src/widgets/type_widgets/Mermaid.tsx b/apps/client/src/widgets/type_widgets/Mermaid.tsx index 65574aa017..9403c4cf61 100644 --- a/apps/client/src/widgets/type_widgets/Mermaid.tsx +++ b/apps/client/src/widgets/type_widgets/Mermaid.tsx @@ -29,6 +29,7 @@ export default function Mermaid(props: TypeWidgetProps) { ); diff --git a/apps/client/src/widgets/type_widgets/code/Code.tsx b/apps/client/src/widgets/type_widgets/code/Code.tsx index 1c08ee8086..b3c8686fae 100644 --- a/apps/client/src/widgets/type_widgets/code/Code.tsx +++ b/apps/client/src/widgets/type_widgets/code/Code.tsx @@ -1,6 +1,7 @@ import "./code.css"; import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror"; +import { NoteType } from "@triliumnext/commons"; import { useEffect, useRef, useState } from "preact/hooks"; import appContext, { CommandListenerData } from "../../../components/app_context"; @@ -24,6 +25,7 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit void; /** Invoked after the content of the note has been uploaded to the server, using a spaced update. */ @@ -72,14 +74,14 @@ function formatViewSource(note: FNote, content: string) { return content; } -export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) { +export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) { const editorRef = useRef(null); const containerRef = useRef(null); const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled"); const mime = useNoteProperty(note, "mime"); const spacedUpdate = useEditorSpacedUpdate({ note, - noteType: "code", + noteType, noteContext, getData: () => ({ content: editorRef.current?.getText() ?? "" }), onContentChange: (content) => { diff --git a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css index 72f680b019..0dce268ea1 100644 --- a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css +++ b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css @@ -15,6 +15,8 @@ .note-detail-split .note-detail-split-editor { width: 100%; flex-grow: 1; + min-width: 0; + min-height: 0; } .note-detail-split .note-detail-split-editor .note-detail-code { @@ -30,6 +32,7 @@ margin: 5px; white-space: pre-wrap; font-size: 0.85em; + overflow: auto; } .note-detail-split .note-detail-split-preview { diff --git a/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx b/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx index 3c9eff27a9..9a1c1dd4e7 100644 --- a/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx +++ b/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "preact/hooks"; +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import { t } from "../../../services/i18n"; import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor"; import { RawHtmlBlock } from "../../react/RawHtml"; @@ -55,7 +55,9 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg, } // Save as attachment. - function onSave() { + const onSave = useCallback(() => { + if (!svg) return; // Don't save if SVG hasn't been rendered yet + const payload = { role: "image", title: `${attachmentName}.svg`, @@ -65,16 +67,18 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg, }; server.post(`notes/${note.noteId}/attachments?matchBy=title`, payload); - } + }, [ svg, attachmentName, note.noteId ]); // Save the SVG when entering a note only when it does not have an attachment. useEffect(() => { + if (!svg) return; // Wait until SVG is rendered + note?.getAttachments().then((attachments) => { if (!attachments.find((a) => a.title === `${attachmentName}.svg`)) { onSave(); } - }); - }, [ note ]); + }).catch(e => console.error("Failed to get attachments for SVGSplitEditor", e)); + }, [ note, svg, attachmentName, onSave ]); // Import/export useTriliumEvent("exportSvg", async({ ntxId: eventNtxId }) => { diff --git a/apps/client/src/widgets/type_widgets/text/EditableText.tsx b/apps/client/src/widgets/type_widgets/text/EditableText.tsx index e5ea9c498e..fba7d6966e 100644 --- a/apps/client/src/widgets/type_widgets/text/EditableText.tsx +++ b/apps/client/src/widgets/type_widgets/text/EditableText.tsx @@ -286,7 +286,7 @@ function useWatchdogCrashHandling() { const currentState = watchdog.state; logInfo(`CKEditor state changed to ${currentState}`); - if (currentState === "ready") { + if (currentState === "ready" && hasCrashed.current) { hasCrashed.current = false; watchdog.editor?.focus(); } diff --git a/docs/Release Notes/!!!meta.json b/docs/Release Notes/!!!meta.json index 55f076f79b..dfe5da6305 100644 --- a/docs/Release Notes/!!!meta.json +++ b/docs/Release Notes/!!!meta.json @@ -1,6 +1,6 @@ { "formatVersion": 2, - "appVersion": "0.101.0", + "appVersion": "0.101.1", "files": [ { "isClone": false, @@ -61,6 +61,32 @@ "attachments": [], "dirFileName": "Release Notes", "children": [ + { + "isClone": false, + "noteId": "vcBthaXcwAm6", + "notePath": [ + "hD3V4hiu2VW4", + "vcBthaXcwAm6" + ], + "title": "v0.101.2", + "notePosition": 10, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [ + { + "type": "relation", + "name": "template", + "value": "wyurrlcDl416", + "isInheritable": false, + "position": 60 + } + ], + "format": "markdown", + "dataFileName": "v0.101.2.md", + "attachments": [] + }, { "isClone": false, "noteId": "AgUcrU9nFXuW", @@ -69,7 +95,7 @@ "AgUcrU9nFXuW" ], "title": "v0.101.1", - "notePosition": 10, + "notePosition": 20, "prefix": null, "isExpanded": false, "type": "text", @@ -95,7 +121,7 @@ "uYwlZ594eyJu" ], "title": "v0.101.0", - "notePosition": 20, + "notePosition": 30, "prefix": null, "isExpanded": false, "type": "text", @@ -121,7 +147,7 @@ "iPGKEk7pwJXK" ], "title": "v0.100.0", - "notePosition": 30, + "notePosition": 40, "prefix": null, "isExpanded": false, "type": "text", @@ -147,7 +173,7 @@ "7HKMTjmopLcM" ], "title": "v0.99.5", - "notePosition": 40, + "notePosition": 50, "prefix": null, "isExpanded": false, "type": "text", @@ -173,7 +199,7 @@ "RMBaNYPsRpIr" ], "title": "v0.99.4", - "notePosition": 50, + "notePosition": 60, "prefix": null, "isExpanded": false, "type": "text", @@ -199,7 +225,7 @@ "yuroLztFfpu5" ], "title": "v0.99.3", - "notePosition": 60, + "notePosition": 70, "prefix": null, "isExpanded": false, "type": "text", @@ -225,7 +251,7 @@ "z207sehwMJ6C" ], "title": "v0.99.2", - "notePosition": 70, + "notePosition": 80, "prefix": null, "isExpanded": false, "type": "text", @@ -251,7 +277,7 @@ "WGQsXq2jNyTi" ], "title": "v0.99.1", - "notePosition": 80, + "notePosition": 90, "prefix": null, "isExpanded": false, "type": "text", @@ -277,7 +303,7 @@ "cyw2Yue9vXf3" ], "title": "v0.99.0", - "notePosition": 90, + "notePosition": 100, "prefix": null, "isExpanded": false, "type": "text", @@ -303,7 +329,7 @@ "QOJwjruOUr4k" ], "title": "v0.98.1", - "notePosition": 100, + "notePosition": 110, "prefix": null, "isExpanded": false, "type": "text", @@ -329,7 +355,7 @@ "PLUoryywi0BC" ], "title": "v0.98.0", - "notePosition": 110, + "notePosition": 120, "prefix": null, "isExpanded": false, "type": "text", @@ -355,7 +381,7 @@ "lvOuiWsLDv8F" ], "title": "v0.97.2", - "notePosition": 120, + "notePosition": 130, "prefix": null, "isExpanded": false, "type": "text", @@ -381,7 +407,7 @@ "OtFZ6Nd9vM3n" ], "title": "v0.97.1", - "notePosition": 130, + "notePosition": 140, "prefix": null, "isExpanded": false, "type": "text", @@ -407,7 +433,7 @@ "SJZ5PwfzHSQ1" ], "title": "v0.97.0", - "notePosition": 140, + "notePosition": 150, "prefix": null, "isExpanded": false, "type": "text", @@ -433,7 +459,7 @@ "mYXFde3LuNR7" ], "title": "v0.96.0", - "notePosition": 150, + "notePosition": 160, "prefix": null, "isExpanded": false, "type": "text", @@ -459,7 +485,7 @@ "jthwbL0FdaeU" ], "title": "v0.95.0", - "notePosition": 160, + "notePosition": 170, "prefix": null, "isExpanded": false, "type": "text", @@ -485,7 +511,7 @@ "7HGYsJbLuhnv" ], "title": "v0.94.1", - "notePosition": 170, + "notePosition": 180, "prefix": null, "isExpanded": false, "type": "text", @@ -511,7 +537,7 @@ "Neq53ujRGBqv" ], "title": "v0.94.0", - "notePosition": 180, + "notePosition": 190, "prefix": null, "isExpanded": false, "type": "text", @@ -537,7 +563,7 @@ "VN3xnce1vLkX" ], "title": "v0.93.0", - "notePosition": 190, + "notePosition": 200, "prefix": null, "isExpanded": false, "type": "text", @@ -555,7 +581,7 @@ "WRaBfQqPr6qo" ], "title": "v0.92.7", - "notePosition": 200, + "notePosition": 210, "prefix": null, "isExpanded": false, "type": "text", @@ -581,7 +607,7 @@ "a2rwfKNmUFU1" ], "title": "v0.92.6", - "notePosition": 210, + "notePosition": 220, "prefix": null, "isExpanded": false, "type": "text", @@ -599,7 +625,7 @@ "fEJ8qErr0BKL" ], "title": "v0.92.5-beta", - "notePosition": 220, + "notePosition": 230, "prefix": null, "isExpanded": false, "type": "text", @@ -617,7 +643,7 @@ "kkkZQQGSXjwy" ], "title": "v0.92.4", - "notePosition": 230, + "notePosition": 240, "prefix": null, "isExpanded": false, "type": "text", @@ -635,7 +661,7 @@ "vAroNixiezaH" ], "title": "v0.92.3-beta", - "notePosition": 240, + "notePosition": 250, "prefix": null, "isExpanded": false, "type": "text", @@ -653,7 +679,7 @@ "mHEq1wxAKNZd" ], "title": "v0.92.2-beta", - "notePosition": 250, + "notePosition": 260, "prefix": null, "isExpanded": false, "type": "text", @@ -671,7 +697,7 @@ "IykjoAmBpc61" ], "title": "v0.92.1-beta", - "notePosition": 260, + "notePosition": 270, "prefix": null, "isExpanded": false, "type": "text", @@ -689,7 +715,7 @@ "dq2AJ9vSBX4Y" ], "title": "v0.92.0-beta", - "notePosition": 270, + "notePosition": 280, "prefix": null, "isExpanded": false, "type": "text", @@ -707,7 +733,7 @@ "3a8aMe4jz4yM" ], "title": "v0.91.6", - "notePosition": 280, + "notePosition": 290, "prefix": null, "isExpanded": false, "type": "text", @@ -725,7 +751,7 @@ "8djQjkiDGESe" ], "title": "v0.91.5", - "notePosition": 290, + "notePosition": 300, "prefix": null, "isExpanded": false, "type": "text", @@ -743,7 +769,7 @@ "OylxVoVJqNmr" ], "title": "v0.91.4-beta", - "notePosition": 300, + "notePosition": 310, "prefix": null, "isExpanded": false, "type": "text", @@ -761,7 +787,7 @@ "tANGQDvnyhrj" ], "title": "v0.91.3-beta", - "notePosition": 310, + "notePosition": 320, "prefix": null, "isExpanded": false, "type": "text", @@ -779,7 +805,7 @@ "hMoBfwSoj1SC" ], "title": "v0.91.2-beta", - "notePosition": 320, + "notePosition": 330, "prefix": null, "isExpanded": false, "type": "text", @@ -797,7 +823,7 @@ "a2XMSKROCl9z" ], "title": "v0.91.1-beta", - "notePosition": 330, + "notePosition": 340, "prefix": null, "isExpanded": false, "type": "text", @@ -815,7 +841,7 @@ "yqXFvWbLkuMD" ], "title": "v0.90.12", - "notePosition": 340, + "notePosition": 350, "prefix": null, "isExpanded": false, "type": "text", @@ -833,7 +859,7 @@ "veS7pg311yJP" ], "title": "v0.90.11-beta", - "notePosition": 350, + "notePosition": 360, "prefix": null, "isExpanded": false, "type": "text", @@ -851,7 +877,7 @@ "sq5W9TQxRqMq" ], "title": "v0.90.10-beta", - "notePosition": 360, + "notePosition": 370, "prefix": null, "isExpanded": false, "type": "text", @@ -869,7 +895,7 @@ "yFEGVCUM9tPx" ], "title": "v0.90.9-beta", - "notePosition": 370, + "notePosition": 380, "prefix": null, "isExpanded": false, "type": "text", @@ -887,7 +913,7 @@ "o4wAGqOQuJtV" ], "title": "v0.90.8", - "notePosition": 380, + "notePosition": 390, "prefix": null, "isExpanded": false, "type": "text", @@ -920,7 +946,7 @@ "i4A5g9iOg9I0" ], "title": "v0.90.7-beta", - "notePosition": 390, + "notePosition": 400, "prefix": null, "isExpanded": false, "type": "text", @@ -938,7 +964,7 @@ "ThNf2GaKgXUs" ], "title": "v0.90.6-beta", - "notePosition": 400, + "notePosition": 410, "prefix": null, "isExpanded": false, "type": "text", @@ -956,7 +982,7 @@ "G4PAi554kQUr" ], "title": "v0.90.5-beta", - "notePosition": 410, + "notePosition": 420, "prefix": null, "isExpanded": false, "type": "text", @@ -983,7 +1009,7 @@ "zATRobGRCmBn" ], "title": "v0.90.4", - "notePosition": 420, + "notePosition": 430, "prefix": null, "isExpanded": false, "type": "text", @@ -1001,7 +1027,7 @@ "sCDLf8IKn3Iz" ], "title": "v0.90.3", - "notePosition": 430, + "notePosition": 440, "prefix": null, "isExpanded": false, "type": "text", @@ -1019,7 +1045,7 @@ "VqqyBu4AuTjC" ], "title": "v0.90.2-beta", - "notePosition": 440, + "notePosition": 450, "prefix": null, "isExpanded": false, "type": "text", @@ -1037,7 +1063,7 @@ "RX3Nl7wInLsA" ], "title": "v0.90.1-beta", - "notePosition": 450, + "notePosition": 460, "prefix": null, "isExpanded": false, "type": "text", @@ -1055,7 +1081,7 @@ "GyueACukPWjk" ], "title": "v0.90.0-beta", - "notePosition": 460, + "notePosition": 470, "prefix": null, "isExpanded": false, "type": "text", @@ -1073,7 +1099,7 @@ "kzjHexDTTeVB" ], "title": "v0.48", - "notePosition": 470, + "notePosition": 480, "prefix": null, "isExpanded": false, "type": "text", @@ -1140,7 +1166,7 @@ "wyurrlcDl416" ], "title": "Release Template", - "notePosition": 480, + "notePosition": 490, "prefix": null, "isExpanded": false, "type": "text", diff --git a/docs/Release Notes/Release Notes/v0.101.2.md b/docs/Release Notes/Release Notes/v0.101.2.md new file mode 100644 index 0000000000..6942b2e8eb --- /dev/null +++ b/docs/Release Notes/Release Notes/v0.101.2.md @@ -0,0 +1,22 @@ +# v0.101.2 +> [!NOTE] +> If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447)  ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links). + +> [!IMPORTANT] +> If you enjoyed this release, consider showing a token of appreciation by: +> +> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right). +> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran). + +## 🐞 Bugfixes + +* [SQL Console: cannot copy table data](https://github.com/TriliumNext/Trilium/pull/8268) by @SiriusXT +* [Title is not selected when creating a note via the launcher](https://github.com/TriliumNext/Trilium/pull/8292) by @SiriusXT +* [Popup editor closing after inserting a note link](https://github.com/TriliumNext/Trilium/pull/8224) by @SiriusXT +* [New Mermaid diagrams do not save content](https://github.com/TriliumNext/Trilium/pull/8220) by @lzinga +* [Can't scroll mermaid diagram code](https://github.com/TriliumNext/Trilium/issues/8299) +* [Max content width is not respected when switching between note types in the same tab](https://github.com/TriliumNext/Trilium/issues/8065) +* [Crash When a Note Includes Itself](https://github.com/TriliumNext/Trilium/issues/8294) +* [Severe Performance Degradation and Crash Issues Due to Recursive Inclusion in Included Notes](https://github.com/TriliumNext/Trilium/issues/8017) +* [ is not a launcher even though it's in the launcher subtree](https://github.com/TriliumNext/Trilium/issues/8218) +* [Archived subnotes of direct children appear in grid view without #includeArchived](https://github.com/TriliumNext/Trilium/issues/8184) \ No newline at end of file