mirror of
https://github.com/zadam/trilium.git
synced 2026-02-17 20:07:01 +01:00
Compare commits
12 Commits
main
...
feature/va
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4e82acc67 | ||
|
|
a5806c0d1d | ||
|
|
bf302a84a9 | ||
|
|
fcc740d592 | ||
|
|
cee16dc3dc | ||
|
|
47601cd1da | ||
|
|
092a60fdd9 | ||
|
|
e8e7568bdc | ||
|
|
4caca56e3b | ||
|
|
e4432e6feb | ||
|
|
d8275e7ea8 | ||
|
|
952dc634b4 |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
82
apps/client/src/services/render.tsx
Normal file
82
apps/client/src/services/render.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
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("}")) {
|
||||
onError?.(JSON.parse(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;
|
||||
return this.props.children;
|
||||
}
|
||||
};
|
||||
const el = h(UserErrorBoundary, {}, h(result as () => VNode, {}));
|
||||
renderReactWidgetAtElement(closestComponent, el, $el[0]);
|
||||
}
|
||||
|
||||
export default {
|
||||
render
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
2
apps/client/src/types.d.ts
vendored
2
apps/client/src/types.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[]) = [
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user