Compare commits

..

1 Commits

Author SHA1 Message Date
Elian Doran
eb5e4fdd37 fix(tests/standalone): keep core specs passing under happy-dom
Patch DOMParser.parseFromString in the standalone vitest setup to strip
the leading LF after <pre>/<listing>/<textarea>, matching the HTML spec
behavior that turnish relies on in Node (domino). Decode attachment
content via decodeUtf8 in the ENEX spec so binary bytes don't get
comma-stringified as a Uint8Array.
2026-04-21 00:14:24 +03:00
28 changed files with 258 additions and 894 deletions

View File

@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(gh issue *)"
]
}
}

View File

@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 1

View File

@@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url";
import { initializeCore, options } from "@triliumnext/core";
import schemaSql from "@triliumnext/core/src/assets/schema.sql?raw";
import HappyDomHtmlParser from "happy-dom/lib/html-parser/HTMLParser.js";
import serverEnTranslations from "../../server/src/assets/translations/en/server.json";
import { beforeAll } from "vitest";
@@ -71,23 +70,17 @@ WebAssembly.instantiateStreaming = (async (source, importObject) => {
// Per HTML5 parsing spec, a single U+000A LINE FEED immediately after a <pre>,
// <listing>, or <textarea> start tag must be ignored ("newlines at the start
// of pre blocks are ignored as an authoring convenience"). Real browsers and
// domino (which the server runtime uses via turnish) both implement this;
// happy-dom (as of 20.8.9) does not — it keeps the LF as a text node.
//
// That difference makes turnish's markdown export produce different output
// under happy-dom vs. production, breaking markdown.spec.ts > "exports jQuery
// code in table properly". Patch HTMLParser.parse to pre-process the string.
// domino (which turnish uses in Node) both implement this; happy-dom does not.
// Patch at the DOMParser boundary since turnish prefers DOMParser when it's
// available — patching via module-level HTMLParser import hits a different
// happy-dom copy than the vitest env loaded.
const LEADING_LF_IN_PRE_RE = /(<(?:pre|listing|textarea)\b[^>]*>)(\r\n|\r|\n)/gi;
const originalHtmlParserParse = (HappyDomHtmlParser as unknown as {
prototype: { parse(html: string, rootNode?: unknown): unknown };
}).prototype.parse;
(HappyDomHtmlParser as unknown as {
prototype: { parse(html: string, rootNode?: unknown): unknown };
}).prototype.parse = function (html: string, rootNode?: unknown) {
const patched = typeof html === "string"
? html.replace(LEADING_LF_IN_PRE_RE, "$1")
: html;
return originalHtmlParserParse.call(this, patched, rootNode);
const originalParseFromString = DOMParser.prototype.parseFromString;
DOMParser.prototype.parseFromString = function (source: string, type: DOMParserSupportedType) {
const patched = typeof source === "string"
? source.replace(LEADING_LF_IN_PRE_RE, "$1")
: source;
return originalParseFromString.call(this, patched, type);
};
// =============================================================================

View File

@@ -51,7 +51,7 @@
"debounce": "3.0.0",
"dompurify": "3.4.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.4",
"force-graph": "1.51.2",
"htmldiff-js": "1.0.5",
"i18next": "26.0.4",
"i18next-http-backend": "3.0.4",

View File

@@ -27,7 +27,7 @@ kbd {
max-width: 350px;
}
/* #region Tree (non-standalone mobile: Fancytree-based) */
/* #region Tree */
.tree-wrapper {
max-height: 100%;
margin-top: 0px;

View File

@@ -1,7 +1,6 @@
import "./mobile_layout.css";
import type AppContext from "../components/app_context.js";
import { isMobileApp } from "../services/utils";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteList from "../widgets/collections/NoteList.jsx";
@@ -16,7 +15,6 @@ import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StandaloneWarningBar from "../widgets/layout/StandaloneWarningBar";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import MobileNoteNavigator from "../widgets/mobile_widgets/MobileNoteNavigator.jsx";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
@@ -26,6 +24,7 @@ import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import { isMobileApp } from "../services/utils";
import ScrollPadding from "../widgets/scroll_padding";
import SearchResult from "../widgets/search_result.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
@@ -49,13 +48,7 @@ export default class MobileLayout {
.css("padding-inline-start", "0")
.css("padding-inline-end", "0")
.css("contain", "content")
.child(
new FlexContainer("column")
.filling()
.id("mobile-sidebar-wrapper")
.child(new QuickSearchWidget())
.child(glob.isStandalone ? <MobileNoteNavigator /> : new NoteTreeWidget())
)
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget()))
)
.child(
new ScreenContainer("detail", "row")

View File

@@ -1861,11 +1861,6 @@
"subtree-hidden-moved-description-collection": "This collection hides its child notes in the tree.",
"subtree-hidden-moved-description-other": "Child notes are hidden in the tree for this note."
},
"mobile_note_navigator": {
"back": "Go up one level",
"loading": "Loading...",
"empty": "This note has no children."
},
"title_bar_buttons": {
"window-on-top": "Keep Window on Top"
},

View File

@@ -207,22 +207,19 @@ function NoteAttributes({ note }: { note: FNote }) {
return <span className="note-list-attributes" ref={ref} />;
}
export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes, showTextRepresentation, onReady }: {
export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes, showTextRepresentation }: {
note: FNote;
trim?: boolean;
noChildrenList?: boolean;
highlightedTokens: string[] | null | undefined;
includeArchivedNotes: boolean;
showTextRepresentation?: boolean;
onReady?: () => void;
}) {
const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
const [ready, setReady] = useState(false);
const [noteType, setNoteType] = useState<string>("none");
const onReadyRef = useRef(onReady);
onReadyRef.current = onReady;
useEffect(() => {
const contentElement = contentRef.current;
@@ -236,7 +233,6 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
}, []);
useEffect(() => {
setReady(false);
content_renderer.getRenderedContent(note, {
trim,
noChildrenList,
@@ -254,14 +250,12 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
highlightSearch(contentRef.current);
setNoteType(type);
setReady(true);
onReadyRef.current?.();
})
.catch(e => {
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
console.error(e);
contentRef.current?.replaceChildren(t("collections.rendering_error"));
setReady(true);
onReadyRef.current?.();
});
}, [ note, highlightedTokens ]);

View File

@@ -1,209 +0,0 @@
.mobile-note-navigator {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
contain: content;
}
.mobile-navigator-toolbar {
display: flex;
align-items: center;
padding: 4px 6px;
border-bottom: 1px solid var(--main-border-color);
background: var(--main-background-color);
}
.mobile-navigator-back {
font-size: 1.6em;
min-width: 40px;
min-height: 40px;
}
.mobile-navigator-back.invisible {
visibility: hidden;
}
.mobile-navigator-scroll {
position: relative;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
overscroll-behavior: contain;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.mobile-navigator-scroll.is-pending .mobile-navigator-body {
visibility: hidden;
}
.mobile-navigator-placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8em;
color: var(--muted-text-color, var(--main-text-color));
pointer-events: none;
}
.mobile-navigator-current-tile {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px;
background: var(--accented-background-color, var(--hover-item-background-color));
border-bottom: 2px solid var(--main-border-color);
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
color: var(--main-text-color);
}
.mobile-navigator-current-tile:active {
filter: brightness(0.95);
}
.mobile-navigator-current-tile.is-active {
background: var(--active-item-background-color);
color: var(--active-item-text-color);
}
.mobile-navigator-current-tile.is-archived {
opacity: 0.7;
}
.mobile-navigator-current-header {
display: flex;
align-items: center;
gap: 10px;
min-height: 32px;
}
.mobile-navigator-current-icon {
flex: 0 0 auto;
font-size: 1.3em;
}
.mobile-navigator-current-title {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.15em;
font-weight: 600;
}
.mobile-navigator-current-open {
flex: 0 0 auto;
font-size: 1.1em;
opacity: 0.6;
}
.mobile-navigator-current-preview {
position: relative;
height: 140px;
overflow: hidden;
font-size: 0.92em;
line-height: 1.35;
color: var(--muted-text-color, var(--main-text-color));
pointer-events: none;
}
/* Hide stale content during the brief window between commit and the new content
being rendered into the DOM, so the preview doesn't flash the previous note's
content before the new one arrives. */
.mobile-navigator-current-preview .note-book-content:not(.note-book-content-ready) {
visibility: hidden;
}
.mobile-navigator-current-preview::after {
content: "";
position: absolute;
inset-inline: 0;
bottom: 0;
height: 32px;
background: linear-gradient(to bottom, transparent, var(--accented-background-color, var(--hover-item-background-color)));
}
.mobile-navigator-current-preview:empty,
.mobile-navigator-current-preview .note-book-content:empty {
display: none;
}
.mobile-navigator-current-preview .note-book-content {
max-width: 100%;
}
.mobile-navigator-current-preview img,
.mobile-navigator-current-preview video {
max-width: 100%;
height: auto;
}
.mobile-navigator-list {
display: flex;
flex-direction: column;
}
.mobile-navigator-row {
display: flex;
align-items: center;
gap: 12px;
min-height: 48px;
padding: 10px 14px;
border-bottom: 1px solid var(--main-border-color);
color: var(--main-text-color);
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.mobile-navigator-row:active {
background: var(--hover-item-background-color);
}
.mobile-navigator-row.is-active {
background: var(--active-item-background-color);
color: var(--active-item-text-color);
}
.mobile-navigator-row.is-archived {
opacity: 0.55;
}
.mobile-navigator-row.is-pending {
cursor: progress;
}
.mobile-navigator-preloader {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
pointer-events: none;
visibility: hidden;
}
.mobile-navigator-row-icon {
flex: 0 0 auto;
font-size: 1.2em;
}
.mobile-navigator-row-title {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1em;
}
.mobile-navigator-row-chevron {
flex: 0 0 auto;
font-size: 1.4em;
opacity: 0.5;
}

View File

@@ -1,368 +0,0 @@
import "./MobileNoteNavigator.css";
import clsx from "clsx";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import type FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import { NoteContent } from "../collections/legacy/ListOrGridView";
import ActionButton from "../react/ActionButton";
import {
useActiveNoteContext,
useNote,
useNoteIcon,
useTriliumEvent,
useTriliumOptionBool
} from "../react/hooks";
import Icon from "../react/Icon";
import NoItems from "../react/NoItems";
/**
* A touch-native replacement for the Fancytree-based note tree on mobile. Shows one
* "column" of children at a time (iOS Files / macOS Finder style): the column's
* current note is rendered at the top with a content preview and opens on tap,
* while tapping child rows drills deeper without navigating.
*/
export default function MobileNoteNavigator() {
const { notePath: activeNotePath, hoistedNoteId, noteContext, parentComponent } = useActiveNoteContext();
const [hideArchived] = useTriliumOptionBool("hideArchivedNotes_main");
const effectiveHoistedId = hoistedNoteId ?? "root";
const [stack, setStack] = useState<string[]>([effectiveHoistedId]);
// When set, a stack we want to navigate to. We render a hidden NoteContent for its
// target to preload the preview, then commit the stack once the preview is ready.
// This keeps the previously-committed view visible during drill/back transitions
// so there's no placeholder flash.
const [nextStack, setNextStack] = useState<string[] | null>(null);
const manualStackRef = useRef(false);
// Sync stack with the active note path unless the user has manually drilled.
useEffect(() => {
if (manualStackRef.current) return;
const newStack = buildStackForActiveNote(activeNotePath, effectiveHoistedId);
setStack(newStack);
setNextStack(null);
}, [activeNotePath, effectiveHoistedId]);
// Rebuild when Froca reports structural changes that could affect the current column.
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const parentPath = stack[stack.length - 1];
const parentNoteId = getLastSegment(parentPath);
if (loadResults.getBranchRows().some((b) => b.parentNoteId === parentNoteId)) {
setStack((prev) => [...prev]);
}
});
const currentParentPath = stack[stack.length - 1];
const currentParentId = getLastSegment(currentParentPath);
const parentNote = useNote(currentParentId);
const parentIcon = useNoteIcon(parentNote);
const activeNoteId = activeNotePath ? getLastSegment(activeNotePath) : undefined;
const pendingPath = nextStack?.[nextStack.length - 1];
const pendingId = pendingPath ? getLastSegment(pendingPath) : undefined;
const pendingNote = useNote(pendingId);
const pendingChildId = nextStack && nextStack.length > stack.length ? pendingId : undefined;
const pendingPop = !!nextStack && nextStack.length < stack.length;
// Froca's initial `tree` response only returns the top of the tree; deeper levels are
// normally pulled in by Fancytree's lazy-load. The navigator doesn't use Fancytree,
// so we pull the subtree ourselves whenever the current parent changes.
const [loadedParents, setLoadedParents] = useState<Set<string>>(() => new Set());
useEffect(() => {
if (!currentParentId || loadedParents.has(currentParentId)) return;
let cancelled = false;
froca.loadSubTree(currentParentId).then(() => {
if (cancelled) return;
setLoadedParents((prev) => {
if (prev.has(currentParentId)) return prev;
const next = new Set(prev);
next.add(currentParentId);
return next;
});
});
return () => {
cancelled = true;
};
}, [currentParentId, loadedParents]);
const isLoaded = !!currentParentId && loadedParents.has(currentParentId);
const children = useNavigatorChildren(parentNote, hideArchived, isLoaded);
const canGoBack = stack.length > 1;
// Gate the visible body on the content preview being ready to avoid a layout shift
// when the preview finishes rendering. Tied to the parent's noteId so a stale "ready"
// flag from the previous column can't leak into the new one.
const [readyForNoteId, setReadyForNoteId] = useState<string | null>(null);
const previewReady = !!parentNote && readyForNoteId === parentNote.noteId;
const bodyVisible = previewReady && isLoaded;
const onPreviewReady = useCallback(() => {
if (parentNote) setReadyForNoteId(parentNote.noteId);
}, [parentNote]);
// Full-screen spinner only for the very first render — once a body has been shown,
// subsequent navigations swap views without a global placeholder.
const hasCommittedOnceRef = useRef(false);
if (bodyVisible) hasCommittedOnceRef.current = true;
const showInitialLoader = !bodyVisible && !hasCommittedOnceRef.current;
// Preload state for the pending stack target. Once ready, commit the stack change.
const [nextReadyNoteId, setNextReadyNoteId] = useState<string | null>(null);
const onPendingReady = useCallback(() => {
if (pendingNote) setNextReadyNoteId(pendingNote.noteId);
}, [pendingNote]);
const scrollRef = useRef<HTMLDivElement>(null);
const bodyRef = useRef<HTMLDivElement>(null);
const directionRef = useRef<"forward" | "backward" | null>(null);
const [commitCounter, setCommitCounter] = useState(0);
useEffect(() => {
if (!nextStack || !pendingId || nextReadyNoteId !== pendingId) return;
setStack(nextStack);
setReadyForNoteId(pendingId);
setNextStack(null);
setNextReadyNoteId(null);
setCommitCounter((c) => c + 1);
// Reset scroll so the committed tile is visible at the top of the column.
if (scrollRef.current) scrollRef.current.scrollTop = 0;
}, [nextStack, pendingId, nextReadyNoteId]);
// Brief slide-in on forward / back so the swap feels directional without blocking the user.
useLayoutEffect(() => {
if (commitCounter === 0) return;
const direction = directionRef.current;
directionRef.current = null;
if (!direction || !bodyRef.current) return;
const offset = direction === "forward" ? "60%" : "-60%";
bodyRef.current.animate(
[
{ transform: `translateX(${offset})`, opacity: 0.4 },
{ transform: "translateX(0)", opacity: 1 }
],
{ duration: 180, easing: "ease-out" }
);
}, [commitCounter]);
const navigateTo = useCallback(
(newStack: string[]) => {
if (hasCommittedOnceRef.current) {
setNextStack(newStack);
} else {
setStack(newStack);
}
},
[]
);
const goBack = useCallback(() => {
if (stack.length <= 1) return;
manualStackRef.current = true;
directionRef.current = "backward";
navigateTo(stack.slice(0, -1));
}, [stack, navigateTo]);
const openNotePath = useCallback(
async (notePath: string) => {
await noteContext?.setNote(notePath);
manualStackRef.current = false;
parentComponent?.triggerCommand("setActiveScreen", { screen: "detail" });
},
[noteContext, parentComponent]
);
const openCurrent = useCallback(() => {
if (!currentParentPath) return;
openNotePath(currentParentPath);
}, [currentParentPath, openNotePath]);
const drillInto = useCallback(
(childNotePath: string) => {
manualStackRef.current = true;
directionRef.current = "forward";
navigateTo([...stack, childNotePath]);
},
[stack, navigateTo]
);
const isCurrentActive = !!activeNoteId && activeNoteId === currentParentId;
return (
<div className="mobile-note-navigator">
<div className="mobile-navigator-toolbar">
<ActionButton
className={clsx("mobile-navigator-back", !canGoBack && "invisible")}
icon={pendingPop ? "bx bx-loader bx-spin" : "bx bx-chevron-left"}
text={t("mobile_note_navigator.back")}
onClick={canGoBack && !pendingPop ? goBack : undefined}
disabled={!canGoBack || pendingPop}
/>
</div>
<div ref={scrollRef} className={clsx("mobile-navigator-scroll", showInitialLoader && "is-pending")}>
<div ref={bodyRef} className="mobile-navigator-body">
{parentNote && (
<div
className={clsx("mobile-navigator-current-tile", {
"is-active": isCurrentActive,
"is-archived": parentNote.isArchived
})}
role="button"
tabIndex={0}
onClick={openCurrent}
>
<div className="mobile-navigator-current-header">
<Icon icon={parentIcon ?? "bx bx-folder"} className="mobile-navigator-current-icon" />
<span className="mobile-navigator-current-title">{parentNote.title}</span>
<Icon icon="bx bx-link-external" className="mobile-navigator-current-open" />
</div>
<div className="mobile-navigator-current-preview">
<NoteContent
note={parentNote}
trim
noChildrenList
highlightedTokens={null}
includeArchivedNotes={!hideArchived}
onReady={onPreviewReady}
/>
</div>
</div>
)}
<div className="mobile-navigator-list">
{!isLoaded || !parentNote ? null : children.length === 0 ? (
<NoItems icon="bx bx-folder-open" text={t("mobile_note_navigator.empty")} />
) : (
children.map((child) => (
<NavigatorRow
key={child.branchId}
note={child.note}
prefix={child.prefix}
childNotePath={`${currentParentPath}/${child.note.noteId}`}
isActive={child.note.noteId === activeNoteId}
isPending={child.note.noteId === pendingChildId}
onDrill={drillInto}
onOpen={openNotePath}
/>
))
)}
</div>
</div>
{showInitialLoader && (
<div className="mobile-navigator-placeholder">
<span className="bx bx-loader bx-spin" />
</div>
)}
</div>
{/* Hidden preloader: renders NoteContent for the pending target so we can
commit the stack change only after its preview is ready. */}
{pendingNote && (
<div className="mobile-navigator-preloader" aria-hidden="true">
<NoteContent
key={pendingNote.noteId}
note={pendingNote}
trim
noChildrenList
highlightedTokens={null}
includeArchivedNotes={!hideArchived}
onReady={onPendingReady}
/>
</div>
)}
</div>
);
}
interface NavigatorRowProps {
note: FNote;
prefix?: string;
childNotePath: string;
isActive: boolean;
isPending: boolean;
onDrill: (notePath: string) => void;
onOpen: (notePath: string) => void;
}
function NavigatorRow({ note, prefix, childNotePath, isActive, isPending, onDrill, onOpen }: NavigatorRowProps) {
const icon = useNoteIcon(note);
const hasChildren = note.hasChildren();
const colorClass = note.getColorClass();
const title = prefix ? `${prefix} - ${note.title}` : note.title;
return (
<div
className={clsx("mobile-navigator-row", colorClass, {
"is-active": isActive,
"is-archived": note.isArchived,
"is-pending": isPending,
"has-children": hasChildren
})}
role="button"
tabIndex={0}
onClick={isPending ? undefined : () => (hasChildren ? onDrill(childNotePath) : onOpen(childNotePath))}
>
<Icon icon={icon ?? "bx bx-note"} className="mobile-navigator-row-icon" />
<span className="mobile-navigator-row-title">{title}</span>
{isPending ? (
<Icon icon="bx bx-loader bx-spin" className="mobile-navigator-row-chevron" />
) : hasChildren ? (
<Icon icon="bx bx-chevron-right" className="mobile-navigator-row-chevron" />
) : null}
</div>
);
}
interface NavigatorChild {
note: FNote;
branchId: string;
prefix?: string;
}
function useNavigatorChildren(parentNote: FNote | null | undefined, hideArchived: boolean, isLoaded: boolean): NavigatorChild[] {
return useMemo(() => {
if (!parentNote || !isLoaded) return [];
const result: NavigatorChild[] = [];
for (const branch of parentNote.getChildBranches()) {
if (!branch) continue;
const note = froca.getNoteFromCache(branch.noteId);
if (!note) continue;
if (note.noteId === "_hidden") continue;
if (hideArchived && note.isArchived) continue;
result.push({ note, branchId: branch.branchId, prefix: branch.prefix });
}
return result;
}, [parentNote, parentNote?.children?.join(","), hideArchived, isLoaded]);
}
function getLastSegment(notePath: string): string {
const idx = notePath.lastIndexOf("/");
return idx >= 0 ? notePath.slice(idx + 1) : notePath;
}
function buildStackForActiveNote(activeNotePath: string | null | undefined, hoistedId: string): string[] {
if (!activeNotePath) return [hoistedId];
const segments = activeNotePath.split("/");
const activeNoteId = segments[segments.length - 1];
const activeNote = froca.getNoteFromCache(activeNoteId);
// If the active note is a folder, show its own children; otherwise show its parent's.
const parentSegments = activeNote?.hasChildren() ? segments : segments.slice(0, -1);
// Clamp to the hoisted root.
let start = parentSegments.indexOf(hoistedId);
if (start < 0) start = 0;
const clamped = parentSegments.slice(start);
if (clamped.length === 0) {
return [hoistedId];
}
const stack: string[] = [];
for (let i = 0; i < clamped.length; i++) {
stack.push(clamped.slice(0, i + 1).join("/"));
}
return stack;
}

View File

@@ -1,4 +1,4 @@
FROM node:24.15.0-bullseye-slim AS builder
FROM node:24.14.1-bullseye-slim AS builder
RUN corepack enable
# Install build tools required to compile native addons (e.g. better-sqlite3) on ARM
@@ -15,7 +15,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.15.0-bullseye-slim
FROM node:24.14.1-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \

View File

@@ -1,4 +1,4 @@
FROM node:24.15.0-alpine AS builder
FROM node:24.14.1-alpine AS builder
RUN corepack enable
# Install build tools required to compile native addons (e.g. better-sqlite3) on ARM
@@ -10,7 +10,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.15.0-alpine
FROM node:24.14.1-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow

View File

@@ -1,4 +1,4 @@
FROM node:24.15.0-alpine AS builder
FROM node:24.14.1-alpine AS builder
RUN corepack enable
# Install build tools required to compile native addons (e.g. better-sqlite3) on ARM
@@ -10,7 +10,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.15.0-alpine
FROM node:24.14.1-alpine
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@@ -1,4 +1,4 @@
FROM node:24.15.0-bullseye-slim AS builder
FROM node:24.14.1-bullseye-slim AS builder
RUN corepack enable
# Install build tools required to compile native addons (e.g. better-sqlite3) on ARM
@@ -15,7 +15,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.15.0-bullseye-slim
FROM node:24.14.1-bullseye-slim
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@@ -58,7 +58,7 @@
"@types/debounce": "1.2.4",
"@types/ejs": "3.1.5",
"@types/express-http-proxy": "1.6.7",
"@types/express-session": "1.19.0",
"@types/express-session": "1.18.2",
"@types/fs-extra": "11.0.4",
"@types/html": "1.0.4",
"@types/ini": "4.1.1",

View File

@@ -16,7 +16,7 @@
"packageManager": "pnpm@10.33.0",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.1",
"wxt": "0.20.23"
"wxt": "0.20.22"
},
"dependencies": {
"cash-dom": "8.1.5"

View File

@@ -19,7 +19,7 @@
"@preact/preset-vite": "2.10.5",
"eslint": "10.2.0",
"eslint-config-preact": "2.0.0",
"typescript": "6.0.3",
"typescript": "6.0.2",
"user-agent-data-types": "0.4.3",
"vite": "8.0.8",
"vitest": "4.1.4"

View File

@@ -20,7 +20,6 @@
"desktop:start-prod": "pnpm run --filter desktop start-prod",
"standalone:start": "pnpm run --filter client-standalone dev",
"standalone:build": "pnpm run --filter client-standalone build",
"standalone:test": "pnpm run --filter client-standalone test",
"mobile:sync": "pnpm run --filter mobile sync",
"desktop:start-prod-no-dir": "pnpm run --filter desktop start-prod-no-dir",
"edit-docs:edit-docs": "pnpm run --filter edit-docs edit-docs",
@@ -80,7 +79,7 @@
"rollup-plugin-webpack-stats": "3.1.1",
"tslib": "2.8.1",
"tsx": "4.21.0",
"typescript": "6.0.3",
"typescript": "6.0.2",
"typescript-eslint": "8.58.2",
"vite": "8.0.8",
"vite-plugin-dts": "4.5.4",

View File

@@ -27,7 +27,7 @@
"eslint-config-ckeditor5": ">=9.1.0",
"stylelint": "17.7.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"typescript": "6.0.3",
"typescript": "6.0.2",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.4",
"webdriverio": "9.27.0"

View File

@@ -28,7 +28,7 @@
"eslint-config-ckeditor5": ">=9.1.0",
"stylelint": "17.7.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"typescript": "6.0.3",
"typescript": "6.0.2",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.4",
"webdriverio": "9.27.0"

View File

@@ -30,7 +30,7 @@
"eslint-config-ckeditor5": ">=9.1.0",
"stylelint": "17.7.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"typescript": "6.0.3",
"typescript": "6.0.2",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.4",
"webdriverio": "9.27.0"

View File

@@ -30,7 +30,7 @@
"eslint-config-ckeditor5": ">=9.1.0",
"stylelint": "17.7.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"typescript": "6.0.3",
"typescript": "6.0.2",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.4",
"webdriverio": "9.27.0"

View File

@@ -30,7 +30,7 @@
"eslint-config-ckeditor5": ">=9.1.0",
"stylelint": "17.7.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"typescript": "6.0.3",
"typescript": "6.0.2",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.4",
"webdriverio": "9.27.0"

View File

@@ -37,6 +37,6 @@
"esbuild": "0.28.0",
"eslint": "10.2.0",
"highlight.js": "11.11.1",
"typescript": "6.0.3"
"typescript": "6.0.2"
}
}

View File

@@ -121,8 +121,3 @@ iframe.pdf-view {
.ck-content .ck-content-widget.footnote-section .ck-content-widget__type-around__button_after {
display: none;
}
.ck-content .todo-list .todo-list__label > input:before {
background-color: var(--background-primary);
border-color: var(--text-primary) !important;
}

View File

@@ -8,6 +8,7 @@ import type BNote from "../../becca/entities/bnote.js";
import { getContext } from "../context.js";
import sql_init from "../sql_init.js";
import TaskContext from "../task_context.js";
import { decodeUtf8 } from "../utils/binary.js";
import enex from "./enex.js";
const scriptDir = dirname(fileURLToPath(import.meta.url));
@@ -61,15 +62,15 @@ describe("importEnex", () => {
const txt = attachments.find(a => a.title === "attachments1.txt");
expect(txt).toBeTruthy();
expect(txt!.mime).toBe("text/plain");
expect(txt!.getContent().toString()).toBe("111");
expect(decodeUtf8(txt!.getContent())).toBe("111");
const bin = attachments.find(a => a.title === "attachments2");
expect(bin).toBeTruthy();
expect(bin!.mime).toBe("application/octet-stream");
expect(bin!.getContent().toString()).toBe("222");
expect(decodeUtf8(bin!.getContent())).toBe("222");
// The note content should contain reference links to the attachments
const content = test1!.getContent().toString();
const content = decodeUtf8(test1!.getContent());
expect(content).toContain(`class="reference-link" href="#root/${test1!.noteId}?viewMode=attachments&amp;attachmentId=${txt!.attachmentId}"`);
expect(content).toContain(`class="reference-link" href="#root/${test1!.noteId}?viewMode=attachments&amp;attachmentId=${bin!.attachmentId}"`);
});

460
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff