New layout: Remove floating buttons (#8059)

This commit is contained in:
Elian Doran
2025-12-15 11:11:18 +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 {
height: 100%;
outline: none !important;
border-radius: 6px;
overflow: hidden;
margin: 4px;
font-size: var(--monospace-font-size);
}
@@ -629,6 +627,11 @@ pre:not(.hljs) {
padding: var(--padding-size);
}
pre:has(> .cm-editor) {
padding: 0;
margin: 0;
}
pre > button.copy-button {
position: absolute;
top: var(--copy-button-margin-size);
@@ -2471,6 +2474,11 @@ footer.webview-footer button {
inset-inline-start: 10px;
}
.content-floating-buttons.top-right {
top: 10px;
inset-inline-end: 10px;
}
.content-floating-buttons.bottom-left {
bottom: 10px;
inset-inline-start: 10px;

View File

@@ -166,17 +166,30 @@ body.desktop .dropdown-submenu .dropdown-menu {
--menu-item-end-padding: 22px;
--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. */
border-radius: 6px;
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-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_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?",
"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"
},
"onclick_button": {

View File

@@ -81,7 +81,7 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
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
text={t("backend_log.refresh")}
icon="bx bx-refresh"
@@ -90,7 +90,7 @@ function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefault
}
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 upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
@@ -103,7 +103,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
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;
return isEnabled && <FloatingButton
@@ -173,7 +173,7 @@ function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }
}
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
icon="bx bx-play"
text={t("code_buttons.execute_button_title")}
@@ -182,7 +182,7 @@ function RunActiveNoteButton({ 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
icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")}
@@ -191,25 +191,29 @@ function OpenTriliumApiDocsButton({ 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
icon="bx bx-save"
text={t("code_buttons.save_to_note_button_title")}
onClick={async (e) => {
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);
}
}}
onClick={buildSaveSqlToNoteHandler(note)}
/>;
}
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) {
const isEnabled = (note.type === "relationMap" && isDefaultViewMode);
const isEnabled = (!isNewLayout && note.type === "relationMap" && isDefaultViewMode);
return isEnabled && (
<>
<FloatingButton
@@ -242,7 +246,7 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB
}
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
const isEnabled = viewType === "geoMap" && !isReadOnly;
const isEnabled = !isNewLayout && viewType === "geoMap" && !isReadOnly;
return isEnabled && (
<FloatingButton
icon="bx bx-plus-circle"
@@ -283,7 +287,7 @@ function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonCon
}
function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) {
const isEnabled = ["mermaid", "mindMap"].includes(note?.type ?? "")
const isEnabled = !isNewLayout && ["mermaid", "mindMap"].includes(note?.type ?? "")
&& note?.isContentAvailable() && isDefaultViewMode;
return isEnabled && (
<>
@@ -304,7 +308,7 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
function InAppHelpButton({ note }: FloatingButtonContext) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl && (!isNewLayout || (note?.type !== "book"));
const isEnabled = !!helpUrl && !isNewLayout;
return isEnabled && (
<FloatingButton

View File

@@ -38,7 +38,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
text={<>
{isVerticalLayout && <VerticalLayoutIcon />}
{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>}
</>}
noDropdownListStyle
@@ -57,7 +57,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
<SwitchToOptions />
<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")} />
<FormDropdownDivider />
@@ -68,19 +68,19 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
{isUpdateAvailable && <>
<FormListHeader text={t("global_menu.new-version-available")} />
<MenuItem command={() => window.open("https://github.com/TriliumNext/Trilium/releases/latest")}
icon="bx bx-download"
text={t("global_menu.download-update", {latestVersion})} />
icon="bx bx-download"
text={t("global_menu.download-update", {latestVersion})} />
</>}
{!isElectron() && <BrowserOnlyOptions />}
{glob.isDev && <DevelopmentOptions />}
{glob.isDev && <DevelopmentOptions dropStart={!isVerticalLayout} />}
</Dropdown>
);
}
function AdvancedMenu() {
function AdvancedMenu({ dropStart }: { dropStart: boolean }) {
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="showSearchHistory" icon="bx bx-search-alt" text={t("global_menu.open_search_history")} />
<FormDropdownDivider />
@@ -103,13 +103,11 @@ function BrowserOnlyOptions() {
</>;
}
function DevelopmentOptions() {
const [ layoutOrientation ] = useTriliumOption("layoutOrientation");
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
return <>
<FormDropdownDivider />
<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) => (
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
))}
@@ -136,10 +134,10 @@ function SwitchToOptions() {
if (isElectron()) {
return;
} else if (!isMobile()) {
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="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_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)>) {
@@ -150,7 +148,7 @@ function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProp
onClick={typeof command === "function" ? command : undefined}
disabled={disabled}
active={active}
>{text}</FormListItem>
>{text}</FormListItem>;
}
function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<KeyboardActionNames>) {
@@ -158,7 +156,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<Keybo
{...props}
command={command}
text={<>{text} <KeyboardShortcut actionName={command as KeyboardActionNames} /></>}
/>
/>;
}
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"/>
</g>
</svg>
)
);
}
function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) {
@@ -205,7 +203,7 @@ function ZoomControls({ parentComponent }: { parentComponent?: Component | null
}}
className={`dropdown-item-button ${icon}`}
>{children}</a>
)
);
}
return isElectron() ? (
@@ -246,7 +244,7 @@ function ToggleWindowOnTop() {
setIsAlwaysOnTop(newState);
}}
/>
)
);
}
function useTriliumUpdateStatus() {
@@ -257,7 +255,7 @@ function useTriliumUpdateStatus() {
async function updateVersionStatus() {
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
let latestVersion: string | undefined = undefined;
let latestVersion: string | undefined;
try {
const resp = await fetch(RELEASES_API_URL);
const data = await resp.json();

View File

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

View File

@@ -2,6 +2,7 @@ import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { useContext } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import branches from "../../services/branches";
@@ -32,7 +33,7 @@ export default function NoteActions() {
<div className="ribbon-button-container" style={{ contain: "none" }}>
{isNewLayout && (
<>
{note && ntxId && <NoteActionsCustom note={note} ntxId={ntxId} />}
{note && ntxId && noteContext && <NoteActionsCustom note={note} ntxId={ntxId} noteContext={noteContext} />}
<MovePaneButton direction="left" />
<MovePaneButton direction="right" />
<ClosePaneButton />
@@ -66,6 +67,8 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
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 isMac = getIsMac();
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"
})} />
{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")} />
<FormDropdownDivider />
@@ -280,3 +284,23 @@ function ConvertToAttachment({ note }: { note: FNote }) {
>{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 { 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 { t } from "../../services/i18n";
import { getHelpUrlForNote } from "../../services/in_app_help";
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 { 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 { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
@@ -14,10 +20,16 @@ import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
interface NoteActionsCustomProps {
note: FNote;
ntxId: string;
noteContext: NoteContext;
}
interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
noteMime: string;
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.
*/
export default function NoteActionsCustom(props: NoteActionsCustomProps) {
const noteType = useNoteProperty(props.note, "type");
const innerProps: NoteActionsCustomInnerProps | undefined = noteType && {
const { note } = props;
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,
noteType
noteType,
noteMime,
viewType: viewType as ViewTypeOptions | null | undefined,
isDefaultViewMode: props.noteContext.viewScope?.viewMode === "default",
parentComponent,
isReadOnly
};
return (innerProps &&
<div className="note-actions-custom">
<AddChildButton {...innerProps} />
<RunActiveNoteButton {...innerProps } />
<OpenTriliumApiDocsButton {...innerProps} />
<SwitchSplitOrientationButton {...innerProps} />
<ToggleReadOnlyButton {...innerProps} />
<SaveToNoteButton {...innerProps} />
<RefreshButton {...innerProps} />
<CopyReferenceToClipboardButton {...innerProps} />
<InAppHelpButton {...innerProps} />
<NoteActionsCustomInner {...innerProps} />
</div>
);
@@ -86,13 +116,13 @@ function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps
);
}
function OpenExternallyButton({ note }: NoteActionsCustomInnerProps) {
function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
return (
<ActionButton
icon="bx bx-link-external"
text={t("file_properties.open")}
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) {
const parentComponent = useContext(ParentComponent);
//#region Floating buttons
function CopyReferenceToClipboardButton({ ntxId, noteType, parentComponent }: NoteActionsCustomInnerProps) {
return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) &&
<ActionButton
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

View File

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

View File

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

View File

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

View File

@@ -43,44 +43,48 @@
/* Horizontal layout */
.note-detail-split.split-horizontal > .note-detail-split-preview-col {
border-inline-start: 1px solid var(--main-border-color);
}
.note-detail-split.split-horizontal:not(.split-read-only) {
&> .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.split-horizontal > .note-detail-split-preview-col {
height: 100%;
width: 50%;
}
&> .note-detail-split-editor-col,
&> .note-detail-split-preview-col {
height: 100%;
width: 50%;
}
.note-detail-split.split-horizontal .note-detail-split-preview {
height: 100%;
.note-detail-split-preview {
height: 100%;
}
}
/* Vertical layout */
.note-detail-split.split-vertical {
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 */
.note-detail-split.split-read-only .note-detail-split-preview-col {
width: 100%;
height: 100%;
}
/* #region SVG */

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 { t } from "../../../services/i18n";
import { CreateChildrenResponse, RelationMapPostResponse } from "@triliumnext/commons";
import { jsPlumbInstance, OnConnectionBindInfo } from "jsplumb";
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 { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
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 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 { 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 {
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 containerRef = useRef<HTMLDivElement>(null);
const mapApiRef = useRef<RelationMapApi>(null);
@@ -119,9 +125,9 @@ export default function RelationMap({ note, noteContext, ntxId }: TypeWidgetProp
options: {
maxZoom: 2,
minZoom: 0.3,
smoothScroll: false,
smoothScroll: false,
//@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
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working
return e.altKey;
@@ -156,6 +162,34 @@ export default function RelationMap({ note, noteContext, ntxId }: TypeWidgetProp
<NoteBox {...note} mapApiRef={mapApiRef} />
))}
</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>
);
}
@@ -380,7 +414,7 @@ function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObjec
// if there's no event, then this has been triggered programmatically
if (!originalEvent || !mapApiRef.current) return;
let name = await dialog.prompt({
const name = await dialog.prompt({
message: t("relation_map.specify_new_relation_name"),
shown: ({ $answer }) => {
if (!$answer) {