Merge branch 'main' into feat/restyle/backlinks-panel

This commit is contained in:
Adorian Doran
2025-12-15 11:54:03 +02:00
committed by GitHub
13 changed files with 356 additions and 129 deletions

View File

@@ -521,9 +521,7 @@ body.mobile .dropdown .dropdown-submenu > span {
.cm-editor { .cm-editor {
height: 100%; height: 100%;
outline: none !important; outline: none !important;
border-radius: 6px;
overflow: hidden; overflow: hidden;
margin: 4px;
font-size: var(--monospace-font-size); font-size: var(--monospace-font-size);
} }
@@ -629,6 +627,11 @@ pre:not(.hljs) {
padding: var(--padding-size); padding: var(--padding-size);
} }
pre:has(> .cm-editor) {
padding: 0;
margin: 0;
}
pre > button.copy-button { pre > button.copy-button {
position: absolute; position: absolute;
top: var(--copy-button-margin-size); top: var(--copy-button-margin-size);
@@ -2471,6 +2474,11 @@ footer.webview-footer button {
inset-inline-start: 10px; inset-inline-start: 10px;
} }
.content-floating-buttons.top-right {
top: 10px;
inset-inline-end: 10px;
}
.content-floating-buttons.bottom-left { .content-floating-buttons.bottom-left {
bottom: 10px; bottom: 10px;
inset-inline-start: 10px; inset-inline-start: 10px;

View File

@@ -166,17 +166,30 @@ body.desktop .dropdown-submenu .dropdown-menu {
--menu-item-end-padding: 22px; --menu-item-end-padding: 22px;
--menu-item-vertical-padding: 2px; --menu-item-vertical-padding: 2px;
padding-top: var(--menu-item-vertical-padding) !important;
padding-bottom: var(--menu-item-vertical-padding) !important;
padding-inline-start: var(--menu-item-start-padding) !important;
padding-inline-end: var(--menu-item-end-padding) !important;
/* Note: the right padding should also accommodate the submenu arrow. */ /* Note: the right padding should also accommodate the submenu arrow. */
border-radius: 6px; border-radius: 6px;
cursor: default !important; cursor: default !important;
} }
body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item { .dropdown-item:not(.dropdown-submenu),
body.desktop .dropdown-item.dropdown-submenu .dropdown-toggle,
.excalidraw .context-menu .context-menu-item {
padding-top: var(--menu-item-vertical-padding) !important;
padding-bottom: var(--menu-item-vertical-padding) !important;
padding-inline-start: var(--menu-item-start-padding) !important;
padding-inline-end: var(--menu-item-end-padding) !important;
}
.dropdown-item.dropdown-submenu {
padding: 0 !important;
.dropdown-toggle {
flex-grow: 1;
}
}
body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item:not(.dropdown-submenu),
body.desktop .dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item.dropdown-submenu .dropdown-toggle {
padding-inline-end: var(--menu-item-start-padding) !important; padding-inline-end: var(--menu-item-start-padding) !important;
padding-inline-start: var(--menu-item-end-padding) !important; padding-inline-start: var(--menu-item-end-padding) !important;
} }

View File

@@ -696,6 +696,9 @@
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.", "convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
"convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?", "convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?",
"print_pdf": "Export as PDF...", "print_pdf": "Export as PDF...",
"export_as_image": "Export as image",
"export_as_image_png": "PNG (raster)",
"export_as_image_svg": "SVG (vector)",
"note_map": "Note map" "note_map": "Note map"
}, },
"onclick_button": { "onclick_button": {

View File

@@ -81,7 +81,7 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [
const isNewLayout = isExperimentalFeatureEnabled("new-layout"); const isNewLayout = isExperimentalFeatureEnabled("new-layout");
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) { function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode; const isEnabled = !isNewLayout && (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
return isEnabled && <FloatingButton return isEnabled && <FloatingButton
text={t("backend_log.refresh")} text={t("backend_log.refresh")}
icon="bx bx-refresh" icon="bx bx-refresh"
@@ -90,7 +90,7 @@ function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefault
} }
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) { function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode; const isEnabled = !isNewLayout && note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode;
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation"); const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal"; const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
@@ -103,7 +103,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) { function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap") const isEnabled = !isNewLayout && ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
&& note.isContentAvailable() && isDefaultViewMode; && note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <FloatingButton return isEnabled && <FloatingButton
@@ -173,7 +173,7 @@ function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }
} }
function RunActiveNoteButton({ note }: FloatingButtonContext) { function RunActiveNoteButton({ note }: FloatingButtonContext) {
const isEnabled = note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium"; const isEnabled = !isNewLayout && (note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium");
return isEnabled && <FloatingButton return isEnabled && <FloatingButton
icon="bx bx-play" icon="bx bx-play"
text={t("code_buttons.execute_button_title")} text={t("code_buttons.execute_button_title")}
@@ -182,7 +182,7 @@ function RunActiveNoteButton({ note }: FloatingButtonContext) {
} }
function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
const isEnabled = note.mime.startsWith("application/javascript;env="); const isEnabled = !isNewLayout && note.mime.startsWith("application/javascript;env=");
return isEnabled && <FloatingButton return isEnabled && <FloatingButton
icon="bx bx-help-circle" icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")} text={t("code_buttons.trilium_api_docs_button_title")}
@@ -191,25 +191,29 @@ function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
} }
function SaveToNoteButton({ note }: FloatingButtonContext) { function SaveToNoteButton({ note }: FloatingButtonContext) {
const isEnabled = note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely(); const isEnabled = !isNewLayout && note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely();
return isEnabled && <FloatingButton return isEnabled && <FloatingButton
icon="bx bx-save" icon="bx bx-save"
text={t("code_buttons.save_to_note_button_title")} text={t("code_buttons.save_to_note_button_title")}
onClick={async (e) => { onClick={buildSaveSqlToNoteHandler(note)}
e.preventDefault();
const { notePath } = await server.post<SaveSqlConsoleResponse>("special-notes/save-sql-console", { sqlConsoleNoteId: note.noteId });
if (notePath) {
toast.showMessage(t("code_buttons.sql_console_saved_message", { "note_path": await tree.getNotePathTitle(notePath) }));
// TODO: This hangs the navigation, for some reason.
//await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
}
}}
/>; />;
} }
export function buildSaveSqlToNoteHandler(note: FNote) {
return async (e: MouseEvent) => {
e.preventDefault();
const { notePath } = await server.post<SaveSqlConsoleResponse>("special-notes/save-sql-console", { sqlConsoleNoteId: note.noteId });
if (notePath) {
toast.showMessage(t("code_buttons.sql_console_saved_message", { "note_path": await tree.getNotePathTitle(notePath) }));
// TODO: This hangs the navigation, for some reason.
//await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
}
};
}
function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingButtonContext) { function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingButtonContext) {
const isEnabled = (note.type === "relationMap" && isDefaultViewMode); const isEnabled = (!isNewLayout && note.type === "relationMap" && isDefaultViewMode);
return isEnabled && ( return isEnabled && (
<> <>
<FloatingButton <FloatingButton
@@ -242,7 +246,7 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB
} }
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) { function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
const isEnabled = viewType === "geoMap" && !isReadOnly; const isEnabled = !isNewLayout && viewType === "geoMap" && !isReadOnly;
return isEnabled && ( return isEnabled && (
<FloatingButton <FloatingButton
icon="bx bx-plus-circle" icon="bx bx-plus-circle"
@@ -283,7 +287,7 @@ function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonCon
} }
function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) { function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = ["mermaid", "mindMap"].includes(note?.type ?? "") const isEnabled = !isNewLayout && ["mermaid", "mindMap"].includes(note?.type ?? "")
&& note?.isContentAvailable() && isDefaultViewMode; && note?.isContentAvailable() && isDefaultViewMode;
return isEnabled && ( return isEnabled && (
<> <>
@@ -304,7 +308,7 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
function InAppHelpButton({ note }: FloatingButtonContext) { function InAppHelpButton({ note }: FloatingButtonContext) {
const helpUrl = getHelpUrlForNote(note); const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl && (!isNewLayout || (note?.type !== "book")); const isEnabled = !!helpUrl && !isNewLayout;
return isEnabled && ( return isEnabled && (
<FloatingButton <FloatingButton

View File

@@ -38,7 +38,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
text={<> text={<>
{isVerticalLayout && <VerticalLayoutIcon />} {isVerticalLayout && <VerticalLayoutIcon />}
{isUpdateAvailable && <div class="global-menu-button-update-available"> {isUpdateAvailable && <div class="global-menu-button-update-available">
<span className="bx bxs-down-arrow-alt global-menu-button-update-available-button" title={t("update_available.update_available")}></span> <span className="bx bxs-down-arrow-alt global-menu-button-update-available-button" title={t("update_available.update_available")} />
</div>} </div>}
</>} </>}
noDropdownListStyle noDropdownListStyle
@@ -57,7 +57,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
<SwitchToOptions /> <SwitchToOptions />
<MenuItem command="showLaunchBarSubtree" icon={`bx ${isMobile() ? "bx-mobile" : "bx-sidebar"}`} text={t("global_menu.configure_launchbar")} /> <MenuItem command="showLaunchBarSubtree" icon={`bx ${isMobile() ? "bx-mobile" : "bx-sidebar"}`} text={t("global_menu.configure_launchbar")} />
<AdvancedMenu /> <AdvancedMenu dropStart={!isVerticalLayout} />
<MenuItem command="showOptions" icon="bx bx-cog" text={t("global_menu.options")} /> <MenuItem command="showOptions" icon="bx bx-cog" text={t("global_menu.options")} />
<FormDropdownDivider /> <FormDropdownDivider />
@@ -68,19 +68,19 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
{isUpdateAvailable && <> {isUpdateAvailable && <>
<FormListHeader text={t("global_menu.new-version-available")} /> <FormListHeader text={t("global_menu.new-version-available")} />
<MenuItem command={() => window.open("https://github.com/TriliumNext/Trilium/releases/latest")} <MenuItem command={() => window.open("https://github.com/TriliumNext/Trilium/releases/latest")}
icon="bx bx-download" icon="bx bx-download"
text={t("global_menu.download-update", {latestVersion})} /> text={t("global_menu.download-update", {latestVersion})} />
</>} </>}
{!isElectron() && <BrowserOnlyOptions />} {!isElectron() && <BrowserOnlyOptions />}
{glob.isDev && <DevelopmentOptions />} {glob.isDev && <DevelopmentOptions dropStart={!isVerticalLayout} />}
</Dropdown> </Dropdown>
); );
} }
function AdvancedMenu() { function AdvancedMenu({ dropStart }: { dropStart: boolean }) {
return ( return (
<FormDropdownSubmenu icon="bx bx-chip" title={t("global_menu.advanced")}> <FormDropdownSubmenu icon="bx bx-chip" title={t("global_menu.advanced")} dropStart={dropStart}>
<MenuItem command="showHiddenSubtree" icon="bx bx-hide" text={t("global_menu.show_hidden_subtree")} /> <MenuItem command="showHiddenSubtree" icon="bx bx-hide" text={t("global_menu.show_hidden_subtree")} />
<MenuItem command="showSearchHistory" icon="bx bx-search-alt" text={t("global_menu.open_search_history")} /> <MenuItem command="showSearchHistory" icon="bx bx-search-alt" text={t("global_menu.open_search_history")} />
<FormDropdownDivider /> <FormDropdownDivider />
@@ -103,13 +103,11 @@ function BrowserOnlyOptions() {
</>; </>;
} }
function DevelopmentOptions() { function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
const [ layoutOrientation ] = useTriliumOption("layoutOrientation");
return <> return <>
<FormDropdownDivider /> <FormDropdownDivider />
<FormListItem disabled>Development Options</FormListItem> <FormListItem disabled>Development Options</FormListItem>
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={layoutOrientation === "horizontal"}> <FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
{experimentalFeatures.map((feature) => ( {experimentalFeatures.map((feature) => (
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} /> <ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
))} ))}
@@ -136,10 +134,10 @@ function SwitchToOptions() {
if (isElectron()) { if (isElectron()) {
return; return;
} else if (!isMobile()) { } else if (!isMobile()) {
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} /> return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />;
} else { }
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} /> return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />;
}
} }
function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) { function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) {
@@ -150,7 +148,7 @@ function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProp
onClick={typeof command === "function" ? command : undefined} onClick={typeof command === "function" ? command : undefined}
disabled={disabled} disabled={disabled}
active={active} active={active}
>{text}</FormListItem> >{text}</FormListItem>;
} }
function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<KeyboardActionNames>) { function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<KeyboardActionNames>) {
@@ -158,7 +156,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<Keybo
{...props} {...props}
command={command} command={command}
text={<>{text} <KeyboardShortcut actionName={command as KeyboardActionNames} /></>} text={<>{text} <KeyboardShortcut actionName={command as KeyboardActionNames} /></>}
/> />;
} }
function VerticalLayoutIcon() { function VerticalLayoutIcon() {
@@ -181,7 +179,7 @@ function VerticalLayoutIcon() {
<path className="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/> <path className="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g> </g>
</svg> </svg>
) );
} }
function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) { function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) {
@@ -205,7 +203,7 @@ function ZoomControls({ parentComponent }: { parentComponent?: Component | null
}} }}
className={`dropdown-item-button ${icon}`} className={`dropdown-item-button ${icon}`}
>{children}</a> >{children}</a>
) );
} }
return isElectron() ? ( return isElectron() ? (
@@ -246,7 +244,7 @@ function ToggleWindowOnTop() {
setIsAlwaysOnTop(newState); setIsAlwaysOnTop(newState);
}} }}
/> />
) );
} }
function useTriliumUpdateStatus() { function useTriliumUpdateStatus() {
@@ -257,7 +255,7 @@ function useTriliumUpdateStatus() {
async function updateVersionStatus() { async function updateVersionStatus() {
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest"; const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
let latestVersion: string | undefined = undefined; let latestVersion: string | undefined;
try { try {
const resp = await fetch(RELEASES_API_URL); const resp = await fetch(RELEASES_API_URL);
const data = await resp.json(); const data = await resp.json();

View File

@@ -217,22 +217,19 @@ export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdo
const [ openOnMobile, setOpenOnMobile ] = useState(false); const [ openOnMobile, setOpenOnMobile ] = useState(false);
return ( return (
<li <li className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })}>
className={clsx("dropdown-item dropdown-submenu", { "submenu-open": openOnMobile, "dropstart": dropStart })} <span
onClick={(e) => { className="dropdown-toggle"
e.stopPropagation(); onClick={(e) => {
if (!isMobile() && onDropdownToggleClicked) {
onDropdownToggleClicked();
}
}}
>
<span className="dropdown-toggle" onClick={(e) => {
if (isMobile()) {
e.stopPropagation(); e.stopPropagation();
setOpenOnMobile(!openOnMobile);
} if (isMobile()) {
}}> setOpenOnMobile(!openOnMobile);
} else if (onDropdownToggleClicked) {
onDropdownToggleClicked();
}
}}
>
<Icon icon={icon} />{" "} <Icon icon={icon} />{" "}
{title} {title}
</span> </span>

View File

@@ -2,6 +2,7 @@ import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context"; import appContext, { CommandNames } from "../../components/app_context";
import Component from "../../components/component";
import NoteContext from "../../components/note_context"; import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import branches from "../../services/branches"; import branches from "../../services/branches";
@@ -32,7 +33,7 @@ export default function NoteActions() {
<div className="ribbon-button-container" style={{ contain: "none" }}> <div className="ribbon-button-container" style={{ contain: "none" }}>
{isNewLayout && ( {isNewLayout && (
<> <>
{note && ntxId && <NoteActionsCustom note={note} ntxId={ntxId} />} {note && ntxId && noteContext && <NoteActionsCustom note={note} ntxId={ntxId} noteContext={noteContext} />}
<MovePaneButton direction="left" /> <MovePaneButton direction="left" />
<MovePaneButton direction="right" /> <MovePaneButton direction="right" />
<ClosePaneButton /> <ClosePaneButton />
@@ -66,6 +67,8 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType); const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help"); const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? "")); const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? ""));
const isExportableToImage = ["mermaid", "mindMap"].includes(noteType);
const isContentAvailable = note.isContentAvailable();
const isElectron = getIsElectron(); const isElectron = getIsElectron();
const isMac = getIsMac(); const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType); const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType);
@@ -110,6 +113,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
defaultType: "single" defaultType: "single"
})} /> })} />
{isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />} {isElectron && <CommandItem command="exportAsPdf" icon="bx bxs-file-pdf" disabled={!isPrintable} text={t("note_actions.print_pdf")} />}
{isExportableToImage && isNormalViewMode && isContentAvailable && <ExportAsImage ntxId={noteContext.ntxId} parentComponent={parentComponent} />}
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} /> <CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
<FormDropdownDivider /> <FormDropdownDivider />
@@ -280,3 +284,23 @@ function ConvertToAttachment({ note }: { note: FNote }) {
>{t("note_actions.convert_into_attachment")}</FormListItem> >{t("note_actions.convert_into_attachment")}</FormListItem>
); );
} }
function ExportAsImage({ ntxId, parentComponent }: { ntxId: string | null | undefined, parentComponent: Component | null | undefined }) {
return (
<FormDropdownSubmenu
icon="bx bxs-file-image"
title={t("note_actions.export_as_image")}
dropStart
>
<FormListItem
icon="bx bxs-file-png"
onClick={() => parentComponent?.triggerEvent("exportPng", { ntxId })}
>{t("note_actions.export_as_image_png")}</FormListItem>
<FormListItem
icon="bx bx-shape-polygon"
onClick={() => parentComponent?.triggerEvent("exportSvg", { ntxId })}
>{t("note_actions.export_as_image_svg")}</FormListItem>
</FormDropdownSubmenu>
);
}

View File

@@ -1,12 +1,18 @@
import { NoteType } from "@triliumnext/commons"; import { NoteType } from "@triliumnext/commons";
import { useContext } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { downloadFileNote, openNoteExternally } from "../../services/open"; import { downloadFileNote, openNoteExternally } from "../../services/open";
import { openInAppHelpFromUrl } from "../../services/utils";
import { ViewTypeOptions } from "../collections/interface";
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
import ActionButton from "../react/ActionButton"; import ActionButton from "../react/ActionButton";
import { FormFileUploadActionButton } from "../react/FormFileUpload"; import { FormFileUploadActionButton } from "../react/FormFileUpload";
import { useNoteProperty } from "../react/hooks"; import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab"; import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab"; import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
@@ -14,10 +20,16 @@ import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
interface NoteActionsCustomProps { interface NoteActionsCustomProps {
note: FNote; note: FNote;
ntxId: string; ntxId: string;
noteContext: NoteContext;
} }
interface NoteActionsCustomInnerProps extends NoteActionsCustomProps { interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
noteMime: string;
noteType: NoteType; noteType: NoteType;
isReadOnly: boolean;
isDefaultViewMode: boolean;
parentComponent: Component;
viewType: ViewTypeOptions | null | undefined;
} }
/** /**
@@ -25,15 +37,33 @@ interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
* from the rest of the note items and the buttons differ based on the note type. * from the rest of the note items and the buttons differ based on the note type.
*/ */
export default function NoteActionsCustom(props: NoteActionsCustomProps) { export default function NoteActionsCustom(props: NoteActionsCustomProps) {
const noteType = useNoteProperty(props.note, "type"); const { note } = props;
const innerProps: NoteActionsCustomInnerProps | undefined = noteType && { const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const [ viewType ] = useNoteLabel(note, "viewType");
const parentComponent = useContext(ParentComponent);
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const innerProps: NoteActionsCustomInnerProps | false = !!noteType && noteMime !== undefined && !!parentComponent && {
...props, ...props,
noteType noteType,
noteMime,
viewType: viewType as ViewTypeOptions | null | undefined,
isDefaultViewMode: props.noteContext.viewScope?.viewMode === "default",
parentComponent,
isReadOnly
}; };
return (innerProps && return (innerProps &&
<div className="note-actions-custom"> <div className="note-actions-custom">
<AddChildButton {...innerProps} />
<RunActiveNoteButton {...innerProps } />
<OpenTriliumApiDocsButton {...innerProps} />
<SwitchSplitOrientationButton {...innerProps} />
<ToggleReadOnlyButton {...innerProps} />
<SaveToNoteButton {...innerProps} />
<RefreshButton {...innerProps} />
<CopyReferenceToClipboardButton {...innerProps} /> <CopyReferenceToClipboardButton {...innerProps} />
<InAppHelpButton {...innerProps} />
<NoteActionsCustomInner {...innerProps} /> <NoteActionsCustomInner {...innerProps} />
</div> </div>
); );
@@ -86,13 +116,13 @@ function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps
); );
} }
function OpenExternallyButton({ note }: NoteActionsCustomInnerProps) { function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
return ( return (
<ActionButton <ActionButton
icon="bx bx-link-external" icon="bx bx-link-external"
text={t("file_properties.open")} text={t("file_properties.open")}
disabled={note.isProtected} disabled={note.isProtected}
onClick={() => openNoteExternally(note.noteId, note.mime)} onClick={() => openNoteExternally(note.noteId, noteMime)}
/> />
); );
} }
@@ -108,9 +138,8 @@ function DownloadFileButton({ note }: NoteActionsCustomInnerProps) {
); );
} }
function CopyReferenceToClipboardButton({ ntxId, noteType }: NoteActionsCustomInnerProps) { //#region Floating buttons
const parentComponent = useContext(ParentComponent); function CopyReferenceToClipboardButton({ ntxId, noteType, parentComponent }: NoteActionsCustomInnerProps) {
return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) && return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) &&
<ActionButton <ActionButton
text={t("image_properties.copy_reference_to_clipboard")} text={t("image_properties.copy_reference_to_clipboard")}
@@ -119,4 +148,111 @@ function CopyReferenceToClipboardButton({ ntxId, noteType }: NoteActionsCustomIn
/> />
); );
} }
function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, noteContext }: NoteActionsCustomInnerProps) {
const isEnabled = (note.noteId === "_backendLog" || noteType === "render") && isDefaultViewMode;
return (isEnabled &&
<ActionButton
text={t("backend_log.refresh")}
icon="bx bx-refresh"
onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })}
/>
);
}
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: NoteActionsCustomInnerProps) {
const isShown = note.type === "mermaid" && note.isContentAvailable() && isDefaultViewMode;
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
return isShown && <ActionButton
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
disabled={isReadOnly}
/>;
}
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <ActionButton
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
onClick={() => setReadOnly(!isReadOnly)}
/>;
}
function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript") || noteMime === "text/x-sqlite;schema=trilium";
return isEnabled && <ActionButton
icon="bx bx-play"
text={t("code_buttons.execute_button_title")}
triggerCommand="runActiveNote"
/>;
}
function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
const [ isEnabled, setIsEnabled ] = useState(false);
function refresh() {
setIsEnabled(noteMime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely());
}
useEffect(refresh, [ note, noteMime ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getBranchRows().find(b => b.noteId === note.noteId)) {
refresh();
}
});
return isEnabled && <ActionButton
icon="bx bx-save"
text={t("code_buttons.save_to_note_button_title")}
onClick={buildSaveSqlToNoteHandler(note)}
/>;
}
function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript;env=");
return isEnabled && <ActionButton
icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")}
onClick={() => openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
/>;
}
function InAppHelpButton({ note, noteType }: NoteActionsCustomInnerProps) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl && (noteType !== "book");
return isEnabled && (
<ActionButton
icon="bx bx-help-circle"
text={t("help-button.title")}
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
/>
);
}
function AddChildButton({ parentComponent, noteType, viewType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) {
if (noteType === "book" && viewType === "geoMap") {
return <ActionButton
icon="bx bx-plus-circle"
text={t("geo-map.create-child-note-title")}
onClick={() => parentComponent.triggerEvent("geoMapCreateChildNote", { ntxId })}
disabled={isReadOnly}
/>;
} else if (noteType === "relationMap") {
return <ActionButton
icon="bx bx-folder-plus"
text={t("relation_map_buttons.create_child_note_title")}
onClick={() => parentComponent.triggerEvent("relationMapCreateChildNote", { ntxId })}
disabled={isReadOnly}
/>;
}
}
//#endregion //#endregion

View File

@@ -451,6 +451,14 @@ body.experimental-feature-new-layout {
margin: 0; margin: 0;
gap: var(--button-gap); gap: var(--button-gap);
button {
transition: opacity 250ms ease-in;
&.disabled {
opacity: 0.4;
}
}
.note-actions-custom { .note-actions-custom {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,10 +1,12 @@
import { useEffect, useRef, useState } from "preact/hooks";
import "./code.css"; import "./code.css";
import { CodeEditor } from "./Code";
import CodeMirror from "@triliumnext/codemirror"; import CodeMirror from "@triliumnext/codemirror";
import { useEffect, useRef, useState } from "preact/hooks";
import server from "../../../services/server"; import server from "../../../services/server";
import { useTriliumEvent } from "../../react/hooks"; import { useTriliumEvent } from "../../react/hooks";
import { TypeWidgetProps } from "../type_widget"; import { TypeWidgetProps } from "../type_widget";
import { CodeEditor } from "./Code";
export default function BackendLog({ ntxId, parentComponent }: TypeWidgetProps) { export default function BackendLog({ ntxId, parentComponent }: TypeWidgetProps) {
const [ content, setContent ] = useState<string>(); const [ content, setContent ] = useState<string>();
@@ -40,5 +42,5 @@ export default function BackendLog({ ntxId, parentComponent }: TypeWidgetProps)
preferPerformance preferPerformance
/> />
</div> </div>
) );
} }

View File

@@ -23,13 +23,9 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
}
.backend-log-editor { .cm-editor {
flex-grow: 1; font-size: 0.85em;
width: 100%; }
border: none;
resize: none;
margin-bottom: 0;
} }
/* #endregion */ /* #endregion */

View File

@@ -43,44 +43,48 @@
/* Horizontal layout */ /* Horizontal layout */
.note-detail-split.split-horizontal > .note-detail-split-preview-col { .note-detail-split.split-horizontal:not(.split-read-only) {
border-inline-start: 1px solid var(--main-border-color); &> .note-detail-split-preview-col {
} border-inline-start: 1px solid var(--main-border-color);
}
.note-detail-split.split-horizontal > .note-detail-split-editor-col, &> .note-detail-split-editor-col,
.note-detail-split.split-horizontal > .note-detail-split-preview-col { &> .note-detail-split-preview-col {
height: 100%; height: 100%;
width: 50%; width: 50%;
} }
.note-detail-split.split-horizontal .note-detail-split-preview { .note-detail-split-preview {
height: 100%; height: 100%;
}
} }
/* Vertical layout */ /* Vertical layout */
.note-detail-split.split-vertical { .note-detail-split.split-vertical {
flex-direction: column; flex-direction: column;
&> .note-detail-split-editor-col,
&> .note-detail-split-preview-col {
width: 100%;
height: 50%;
}
&> .note-detail-split-editor-col {
border-top: 1px solid var(--main-border-color);
}
&> .note-detail-split-preview-col {
order: -1;
}
} }
.note-detail-split.split-vertical > .note-detail-split-editor-col,
.note-detail-split.split-vertical > .note-detail-split-preview-col {
width: 100%;
height: 50%;
}
.note-detail-split.split-vertical > .note-detail-split-editor-col {
border-top: 1px solid var(--main-border-color);
}
.note-detail-split.split-vertical .note-detail-split-preview-col {
order: -1;
}
/* Read-only view */ /* Read-only view */
.note-detail-split.split-read-only .note-detail-split-preview-col { .note-detail-split.split-read-only .note-detail-split-preview-col {
width: 100%; width: 100%;
height: 100%;
} }
/* #region SVG */ /* #region SVG */
@@ -93,4 +97,4 @@
height: 100%; height: 100%;
max-width: 100%; max-width: 100%;
} }
/* #endregion */ /* #endregion */

View File

@@ -1,25 +1,31 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { TypeWidgetProps } from "../type_widget";
import { jsPlumbInstance, OnConnectionBindInfo } from "jsplumb";
import { useEditorSpacedUpdate, useTriliumEvent, useTriliumEvents } from "../../react/hooks";
import FNote from "../../../entities/fnote";
import { RefObject } from "preact";
import "./RelationMap.css"; import "./RelationMap.css";
import { t } from "../../../services/i18n";
import { CreateChildrenResponse, RelationMapPostResponse } from "@triliumnext/commons";
import { jsPlumbInstance, OnConnectionBindInfo } from "jsplumb";
import panzoom, { PanZoomOptions } from "panzoom"; import panzoom, { PanZoomOptions } from "panzoom";
import { RefObject } from "preact";
import { HTMLProps } from "preact/compat";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import attribute_autocomplete from "../../../services/attribute_autocomplete";
import dialog from "../../../services/dialog"; import dialog from "../../../services/dialog";
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
import { t } from "../../../services/i18n";
import server from "../../../services/server"; import server from "../../../services/server";
import toast from "../../../services/toast"; import toast from "../../../services/toast";
import { CreateChildrenResponse, RelationMapPostResponse } from "@triliumnext/commons";
import RelationMapApi, { ClientRelation, MapData, MapDataNoteEntry, RelationType } from "./api";
import setupOverlays, { uniDirectionalOverlays } from "./overlays";
import { JsPlumb } from "./jsplumb";
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
import { NoteBox } from "./NoteBox";
import utils from "../../../services/utils"; import utils from "../../../services/utils";
import attribute_autocomplete from "../../../services/attribute_autocomplete"; import ActionButton from "../../react/ActionButton";
import { useEditorSpacedUpdate, useTriliumEvent, useTriliumEvents } from "../../react/hooks";
import { TypeWidgetProps } from "../type_widget";
import RelationMapApi, { ClientRelation, MapData, MapDataNoteEntry, RelationType } from "./api";
import { buildRelationContextMenuHandler } from "./context_menu"; import { buildRelationContextMenuHandler } from "./context_menu";
import { HTMLProps } from "preact/compat"; import { JsPlumb } from "./jsplumb";
import { NoteBox } from "./NoteBox";
import setupOverlays, { uniDirectionalOverlays } from "./overlays";
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
interface Clipboard { interface Clipboard {
noteId: string; noteId: string;
@@ -43,7 +49,7 @@ declare module "jsplumb" {
} }
} }
export default function RelationMap({ note, noteContext, ntxId }: TypeWidgetProps) { export default function RelationMap({ note, noteContext, ntxId, parentComponent }: TypeWidgetProps) {
const [ data, setData ] = useState<MapData>(); const [ data, setData ] = useState<MapData>();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const mapApiRef = useRef<RelationMapApi>(null); const mapApiRef = useRef<RelationMapApi>(null);
@@ -119,9 +125,9 @@ export default function RelationMap({ note, noteContext, ntxId }: TypeWidgetProp
options: { options: {
maxZoom: 2, maxZoom: 2,
minZoom: 0.3, minZoom: 0.3,
smoothScroll: false, smoothScroll: false,
//@ts-expect-error Upstream incorrectly mentions no arguments. //@ts-expect-error Upstream incorrectly mentions no arguments.
filterKey: function (e: KeyboardEvent) { filterKey (e: KeyboardEvent) {
// if ALT is pressed, then panzoom should bubble the event up // if ALT is pressed, then panzoom should bubble the event up
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working // this is to preserve ALT-LEFT, ALT-RIGHT navigation working
return e.altKey; return e.altKey;
@@ -156,6 +162,34 @@ export default function RelationMap({ note, noteContext, ntxId }: TypeWidgetProp
<NoteBox {...note} mapApiRef={mapApiRef} /> <NoteBox {...note} mapApiRef={mapApiRef} />
))} ))}
</JsPlumb> </JsPlumb>
{isNewLayout && (
<div className="btn-group btn-group-sm content-floating-buttons bottom-right">
<ActionButton
icon="bx bx-zoom-in"
text={t("relation_map_buttons.zoom_in_title")}
onClick={() => parentComponent?.triggerEvent("relationMapResetZoomIn", { ntxId })}
className="tn-tool-button"
noIconActionClass
/>
<ActionButton
icon="bx bx-zoom-out"
text={t("relation_map_buttons.zoom_out_title")}
onClick={() => parentComponent?.triggerEvent("relationMapResetZoomOut", { ntxId })}
className="tn-tool-button"
noIconActionClass
/>
<ActionButton
icon="bx bx-crop"
text={t("relation_map_buttons.reset_pan_zoom_title")}
onClick={() => parentComponent?.triggerEvent("relationMapResetPanZoom", { ntxId })}
className="tn-tool-button"
noIconActionClass
/>
</div>
)}
</div> </div>
); );
} }
@@ -380,7 +414,7 @@ function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObjec
// if there's no event, then this has been triggered programmatically // if there's no event, then this has been triggered programmatically
if (!originalEvent || !mapApiRef.current) return; if (!originalEvent || !mapApiRef.current) return;
let name = await dialog.prompt({ const name = await dialog.prompt({
message: t("relation_map.specify_new_relation_name"), message: t("relation_map.specify_new_relation_name"),
shown: ({ $answer }) => { shown: ({ $answer }) => {
if (!$answer) { if (!$answer) {