Various bugfixes for v0.102.0 (#8748)
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Documentation / Build and Deploy Documentation (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run

This commit is contained in:
Elian Doran 2026-02-17 23:40:02 +02:00 committed by GitHub
commit 3c3e73edae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 202 additions and 110 deletions

View File

@ -2,7 +2,6 @@ import { h, VNode } from "preact";
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
import RightPanelWidget from "../widgets/right_panel_widget.js";
import froca from "./froca.js";
import type { Entity } from "./frontend_script_api.js";
import { WidgetDefinitionWithType } from "./frontend_script_api_preact.js";
import { t } from "./i18n.js";
@ -38,15 +37,18 @@ async function getAndExecuteBundle(noteId: string, originEntity = null, script =
export type ParentName = "left-pane" | "center-pane" | "note-detail-pane" | "right-pane";
export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
export async function executeBundleWithoutErrorHandling(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
return await function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext);
}
export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
try {
return await function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext);
} catch (e: any) {
showErrorForScriptNote(bundle.noteId, t("toast.bundle-error.message", { message: e.message }));
return await executeBundleWithoutErrorHandling(bundle, originEntity, $container);
} catch (e: unknown) {
showErrorForScriptNote(bundle.noteId, t("toast.bundle-error.message", { message: getErrorMessage(e) }));
logError("Widget initialization failed: ", e);
}
}

View File

@ -15,7 +15,7 @@ import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import renderService from "./render.js";
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import utils from "./utils.js";
import utils, { getErrorMessage } from "./utils.js";
let idCounter = 1;
@ -62,7 +62,10 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
} else if (type === "render" && entity instanceof FNote) {
const $content = $("<div>");
await renderService.render(entity, $content);
await renderService.render(entity, $content, (e) => {
const $error = $("<div>").addClass("admonition caution").text(typeof e === "string" ? e : getErrorMessage(e));
$content.empty().append($error);
});
$renderedContent.append($content);
} else if (type === "doc" && "noteId" in entity) {

View File

@ -1,56 +0,0 @@
import { h, VNode } from "preact";
import type FNote from "../entities/fnote.js";
import { renderReactWidgetAtElement } from "../widgets/react/react_utils.jsx";
import bundleService, { type Bundle } from "./bundle.js";
import froca from "./froca.js";
import server from "./server.js";
async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: (e: unknown) => void) {
const relations = note.getRelations("renderNote");
const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId);
$el.empty().toggle(renderNoteIds.length > 0);
for (const renderNoteId of renderNoteIds) {
const bundle = await server.post<Bundle>(`script/bundle/${renderNoteId}`);
const $scriptContainer = $("<div>");
$el.append($scriptContainer);
$scriptContainer.append(bundle.html);
// async so that scripts cannot block trilium execution
bundleService.executeBundle(bundle, note, $scriptContainer)
.catch(onError)
.then(result => {
// Render JSX
if (bundle.html === "") {
renderIfJsx(bundle, result, $el).catch(onError);
}
});
}
return renderNoteIds.length > 0;
}
async function renderIfJsx(bundle: Bundle, result: unknown, $el: JQuery<HTMLElement>) {
// Ensure the root script note is actually a JSX.
const rootScriptNoteId = await froca.getNote(bundle.noteId);
if (rootScriptNoteId?.mime !== "text/jsx") return;
// Ensure the output is a valid el.
if (typeof result !== "function") return;
// Obtain the parent component.
const closestComponent = glob.getComponentByEl($el.closest(".component")[0]);
if (!closestComponent) return;
// Render the element.
const el = h(result as () => VNode, {});
renderReactWidgetAtElement(closestComponent, el, $el[0]);
}
export default {
render
};

View File

@ -0,0 +1,86 @@
import { Component, h, VNode } from "preact";
import type FNote from "../entities/fnote.js";
import { renderReactWidgetAtElement } from "../widgets/react/react_utils.jsx";
import { type Bundle, executeBundleWithoutErrorHandling } from "./bundle.js";
import froca from "./froca.js";
import server from "./server.js";
type ErrorHandler = (e: unknown) => void;
async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: ErrorHandler) {
const relations = note.getRelations("renderNote");
const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId);
$el.empty().toggle(renderNoteIds.length > 0);
try {
for (const renderNoteId of renderNoteIds) {
const bundle = await server.postWithSilentInternalServerError<Bundle>(`script/bundle/${renderNoteId}`);
const $scriptContainer = $("<div>");
$el.append($scriptContainer);
$scriptContainer.append(bundle.html);
// async so that scripts cannot block trilium execution
executeBundleWithoutErrorHandling(bundle, note, $scriptContainer)
.catch(onError)
.then(result => {
// Render JSX
if (bundle.html === "") {
renderIfJsx(bundle, result, $el, onError).catch(onError);
}
});
}
return renderNoteIds.length > 0;
} catch (e) {
if (typeof e === "string" && e.startsWith("{") && e.endsWith("}")) {
try {
onError?.(JSON.parse(e));
} catch (e) {
onError?.(e);
}
} else {
onError?.(e);
}
}
}
async function renderIfJsx(bundle: Bundle, result: unknown, $el: JQuery<HTMLElement>, onError?: ErrorHandler) {
// Ensure the root script note is actually a JSX.
const rootScriptNoteId = await froca.getNote(bundle.noteId);
if (rootScriptNoteId?.mime !== "text/jsx") return;
// Ensure the output is a valid el.
if (typeof result !== "function") return;
// Obtain the parent component.
const closestComponent = glob.getComponentByEl($el.closest(".component")[0]);
if (!closestComponent) return;
// Render the element.
const UserErrorBoundary = class UserErrorBoundary extends Component {
constructor(props: object) {
super(props);
this.state = { error: null };
}
componentDidCatch(error: unknown) {
onError?.(error);
this.setState({ error });
}
render() {
if ("error" in this.state && this.state?.error) return null;
return this.props.children;
}
};
const el = h(UserErrorBoundary, {}, h(result as () => VNode, {}));
renderReactWidgetAtElement(closestComponent, el, $el[0]);
}
export default {
render
};

View File

@ -73,6 +73,10 @@ async function post<T>(url: string, data?: unknown, componentId?: string) {
return await call<T>("POST", url, componentId, { data });
}
async function postWithSilentInternalServerError<T>(url: string, data?: unknown, componentId?: string) {
return await call<T>("POST", url, componentId, { data, silentInternalServerError: true });
}
async function put<T>(url: string, data?: unknown, componentId?: string) {
return await call<T>("PUT", url, componentId, { data });
}
@ -111,6 +115,7 @@ let maxKnownEntityChangeId = 0;
interface CallOptions {
data?: unknown;
silentNotFound?: boolean;
silentInternalServerError?: boolean;
// If `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc.
raw?: boolean;
}
@ -143,7 +148,7 @@ async function call<T>(method: string, url: string, componentId?: string, option
});
})) as any;
} else {
resp = await ajax(url, method, data, headers, !!options.silentNotFound, options.raw);
resp = await ajax(url, method, data, headers, options);
}
const maxEntityChangeIdStr = resp.headers["trilium-max-entity-change-id"];
@ -155,10 +160,7 @@ async function call<T>(method: string, url: string, componentId?: string, option
return resp.body as T;
}
/**
* @param raw if `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc.
*/
function ajax(url: string, method: string, data: unknown, headers: Headers, silentNotFound: boolean, raw?: boolean): Promise<Response> {
function ajax(url: string, method: string, data: unknown, headers: Headers, opts: CallOptions): Promise<Response> {
return new Promise((res, rej) => {
const options: JQueryAjaxSettings = {
url: window.glob.baseApiUrl + url,
@ -190,7 +192,9 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
// don't report requests that are rejected by the browser, usually when the user is refreshing or going to a different page.
rej("rejected by browser");
return;
} else if (silentNotFound && jqXhr.status === 404) {
} else if (opts.silentNotFound && jqXhr.status === 404) {
// report nothing
} else if (opts.silentInternalServerError && jqXhr.status === 500) {
// report nothing
} else {
await reportError(method, url, jqXhr.status, jqXhr.responseText);
@ -200,7 +204,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
}
};
if (raw) {
if (opts.raw) {
options.dataType = "text";
}
@ -299,6 +303,7 @@ export default {
get,
getWithSilentNotFound,
post,
postWithSilentInternalServerError,
put,
patch,
remove,

View File

@ -63,11 +63,13 @@ declare global {
declare module "preact" {
namespace JSX {
interface ElectronWebViewElement extends JSX.HTMLAttributes<HTMLElement> {
src: string;
class: string;
}
interface IntrinsicElements {
webview: {
src: string;
class: string;
}
webview: ElectronWebViewElement;
}
}
}

View File

@ -119,7 +119,7 @@ declare global {
setNote(noteId: string);
}
var logError: (message: string, e?: Error | string) => void;
var logError: (message: string, e?: unknown) => void;
var logInfo: (message: string) => void;
var glob: CustomGlobals;
//@ts-ignore

View File

@ -1,12 +1,14 @@
import { useEffect, useRef, useState } from "preact/hooks";
import "./SyncStatus.css";
import { t } from "../../services/i18n";
import clsx from "clsx";
import { escapeQuotes } from "../../services/utils";
import { useStaticTooltip, useTriliumOption } from "../react/hooks";
import sync from "../../services/sync";
import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws";
import { WebSocketMessage } from "@triliumnext/commons";
import clsx from "clsx";
import { useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import sync from "../../services/sync";
import { escapeQuotes } from "../../services/utils";
import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws";
import { useStaticTooltip, useTriliumOption } from "../react/hooks";
type SyncState = "unknown" | "in-progress"
| "connected-with-changes" | "connected-no-changes"
@ -53,29 +55,29 @@ export default function SyncStatus() {
const spanRef = useRef<HTMLSpanElement>(null);
const [ syncServerHost ] = useTriliumOption("syncServerHost");
useStaticTooltip(spanRef, {
html: true
// TODO: Placement
html: true,
title: escapeQuotes(title)
});
return (syncServerHost &&
<div class="sync-status-widget launcher-button">
<div class="sync-status">
<span
key={syncState} // Force re-render when state changes to update tooltip content.
ref={spanRef}
className={clsx("sync-status-icon", `sync-status-${syncState}`, icon)}
title={escapeQuotes(title)}
onClick={() => {
if (syncState === "in-progress") return;
sync.syncNow();
}}
>
{hasChanges && (
<span class="bx bxs-star sync-status-sub-icon"></span>
<span class="bx bxs-star sync-status-sub-icon" />
)}
</span>
</div>
</div>
)
);
}
function useSyncStatus() {

View File

@ -9,7 +9,7 @@ import { openInAppHelpFromUrl } from "../../services/utils";
import { BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useNoteContext, useTriliumEvent } from "../react/hooks";
import { useNoteContext, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { BookProperty, ViewProperty } from "../react/NotePropertyMenu";
const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ];
@ -213,6 +213,8 @@ function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentI
function useActiveContentInfo(note: FNote | null | undefined) {
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
function refresh() {
let type: ActiveContentInfo["type"] | null = null;
@ -224,13 +226,13 @@ function useActiveContentInfo(note: FNote | null | undefined) {
return;
}
if (note.type === "render") {
if (noteType === "render") {
type = "renderNote";
isEnabled = note.hasRelation("renderNote");
} else if (note.type === "webView") {
} else if (noteType === "webView") {
type = "webView";
isEnabled = note.hasLabel("webViewSrc");
} else if (note.type === "code" && note.mime === "application/javascript;env=backend") {
} else if (noteType === "code" && noteMime === "application/javascript;env=backend") {
type = "backendScript";
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
isEnabled ||= note.hasLabel(backendLabel);
@ -239,11 +241,11 @@ function useActiveContentInfo(note: FNote | null | undefined) {
canToggleEnabled = true;
}
}
} else if (note.type === "code" && note.mime === "application/javascript;env=frontend") {
} else if (noteType === "code" && noteMime === "application/javascript;env=frontend") {
type = "frontendScript";
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
} else if (note.type === "code" && note.hasLabelOrDisabled("appTheme")) {
} else if (noteType === "code" && note.hasLabelOrDisabled("appTheme")) {
isEnabled = note.hasLabel("appTheme");
canToggleEnabled = true;
}
@ -270,7 +272,7 @@ function useActiveContentInfo(note: FNote | null | undefined) {
}
// Refresh on note change.
useEffect(refresh, [ note ]);
useEffect(refresh, [ note, noteType, noteMime ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {

View File

@ -19,6 +19,9 @@
--link-hover-background: var(--icon-button-hover-background);
color: var(--custom-color, inherit);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
color: var(--custom-color, inherit);

View File

@ -1,7 +1,8 @@
import "./WebView.css";
import { useCallback, useState } from "preact/hooks";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
@ -17,7 +18,7 @@ import { TypeWidgetProps } from "./type_widget";
const isElectron = utils.isElectron();
const HELP_PAGE = "1vHRoWCEjj0L";
export default function WebView({ note }: TypeWidgetProps) {
export default function WebView({ note, ntxId }: TypeWidgetProps) {
const [ webViewSrc ] = useNoteLabel(note, "webViewSrc");
const [ disabledWebViewSrc ] = useNoteLabel(note, "disabled:webViewSrc");
@ -29,15 +30,58 @@ export default function WebView({ note }: TypeWidgetProps) {
return <SetupWebView note={note} />;
}
return <WebViewContent src={webViewSrc} />;
return isElectron
? <DesktopWebView src={webViewSrc} ntxId={ntxId} />
: <BrowserWebView src={webViewSrc} ntxId={ntxId} />;
}
function WebViewContent({ src }: { src: string }) {
if (!isElectron) {
return <iframe src={src} class="note-detail-web-view-content" sandbox="allow-same-origin allow-scripts allow-popups" />;
}
return <webview src={src} class="note-detail-web-view-content" />;
function DesktopWebView({ src, ntxId }: { src: string, ntxId: string | null | undefined }) {
const webviewRef = useRef<HTMLWebViewElement>(null);
useEffect(() => {
const webview = webviewRef.current;
if (!webview) return;
function onBlur() {
if (document.activeElement === webview && ntxId) {
appContext.tabManager.activateNoteContext(ntxId);
}
}
webview.addEventListener("focus", onBlur);
return () => {
webview.removeEventListener("focus", onBlur);
};
}, [ ntxId ]);
return <webview
ref={webviewRef}
src={src}
class="note-detail-web-view-content"
/>;
}
function BrowserWebView({ src, ntxId }: { src: string, ntxId: string | null | undefined }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
function onBlur() {
if (document.activeElement === iframeRef.current && ntxId) {
appContext.tabManager.activateNoteContext(ntxId);
}
}
window.addEventListener("blur", onBlur);
return () => {
window.removeEventListener("blur", onBlur);
};
}, [ ntxId ]);
return <iframe
ref={iframeRef}
src={src}
class="note-detail-web-view-content"
sandbox="allow-same-origin allow-scripts allow-popups" />;
}
function SetupWebView({note}: {note: FNote}) {

View File

@ -180,7 +180,8 @@ export function CodeEditor({ parentComponent, ntxId, containerRef: externalConta
resolve(refToJQuerySelector(containerRef));
});
useTriliumEvent("scrollToEnd", () => {
useTriliumEvent("scrollToEnd", ({ ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId) return;
const editor = codeEditorRef.current;
if (!editor) return;
editor.scrollToEnd();

View File

@ -38,8 +38,8 @@ async function register(app: express.Application) {
base: `/${assetUrlFragment}/`
});
app.use(`/${assetUrlFragment}/`, (req, res, next) => {
if (req.url.startsWith("/images/")) {
// Images are served as static assets from the server.
if (req.url.startsWith("/images/") || req.url.startsWith("/doc_notes/")) {
// Images and doc notes are served as static assets from the server.
next();
return;
}

View File

@ -21,7 +21,7 @@ export async function lint(mimeType: string) {
if (mimeType === "application/javascript;env=frontend") {
globals = { ...globals, ...globalDefinitions.jquery };
} else if (mimeType === "application/javascript;env=backend") {
globals = { ...globals, ...globalDefinitions.nodeBuiltin };
}
const config: (Linter.LegacyConfig | Linter.Config | Linter.Config[]) = [

View File

@ -188,8 +188,6 @@ export default class CodeMirror extends EditorView {
const endPos = this.state.doc.length;
this.dispatch({
selection: EditorSelection.cursor(endPos),
effects: EditorView.scrollIntoView(endPos, { y: "end" }),
scrollIntoView: true
});
}