diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 8ecd85b69..b7c056248 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -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; diff --git a/apps/client/src/stylesheets/theme-next/base.css b/apps/client/src/stylesheets/theme-next/base.css index e210b8d12..e3468421e 100644 --- a/apps/client/src/stylesheets/theme-next/base.css +++ b/apps/client/src/stylesheets/theme-next/base.css @@ -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; } diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 9ecdcd640..e404651e6 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -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": { diff --git a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx index c30c9f338..4cd6e1c5f 100644 --- a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx +++ b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx @@ -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 && { - e.preventDefault(); - const { notePath } = await server.post("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("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 && ( <> @@ -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 && ( {isVerticalLayout && } {isUpdateAvailable && } } noDropdownListStyle @@ -57,7 +57,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: - + @@ -68,19 +68,19 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: {isUpdateAvailable && <> 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() && } - {glob.isDev && } + {glob.isDev && } ); } -function AdvancedMenu() { +function AdvancedMenu({ dropStart }: { dropStart: boolean }) { return ( - + @@ -103,13 +103,11 @@ function BrowserOnlyOptions() { ; } -function DevelopmentOptions() { - const [ layoutOrientation ] = useTriliumOption("layoutOrientation"); - +function DevelopmentOptions({ dropStart }: { dropStart: boolean }) { return <> Development Options - + {experimentalFeatures.map((feature) => ( ))} @@ -136,10 +134,10 @@ function SwitchToOptions() { if (isElectron()) { return; } else if (!isMobile()) { - return - } else { - return - } + return ; + } + return ; + } function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps 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} + >{text}; } function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps) { @@ -158,7 +156,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps{text} } - /> + />; } function VerticalLayoutIcon() { @@ -181,7 +179,7 @@ function VerticalLayoutIcon() { - ) + ); } function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) { @@ -205,7 +203,7 @@ function ZoomControls({ parentComponent }: { parentComponent?: Component | null }} className={`dropdown-item-button ${icon}`} >{children} - ) + ); } 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(); diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index 87a047821..dd40bd08c 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -217,22 +217,19 @@ export function FormDropdownSubmenu({ icon, title, children, dropStart, onDropdo const [ openOnMobile, setOpenOnMobile ] = useState(false); return ( -
  • { - e.stopPropagation(); - - if (!isMobile() && onDropdownToggleClicked) { - onDropdownToggleClicked(); - } - }} - > - { - if (isMobile()) { +
  • + { e.stopPropagation(); - setOpenOnMobile(!openOnMobile); - } - }}> + + if (isMobile()) { + setOpenOnMobile(!openOnMobile); + } else if (onDropdownToggleClicked) { + onDropdownToggleClicked(); + } + }} + > {" "} {title} diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index 698945d8d..bb6d9a8d8 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -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() {
    {isNewLayout && ( <> - {note && ntxId && } + {note && ntxId && noteContext && } @@ -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 && } + {isExportableToImage && isNormalViewMode && isContentAvailable && } @@ -280,3 +284,23 @@ function ConvertToAttachment({ note }: { note: FNote }) { >{t("note_actions.convert_into_attachment")} ); } + +function ExportAsImage({ ntxId, parentComponent }: { ntxId: string | null | undefined, parentComponent: Component | null | undefined }) { + return ( + + parentComponent?.triggerEvent("exportPng", { ntxId })} + >{t("note_actions.export_as_image_png")} + + parentComponent?.triggerEvent("exportSvg", { ntxId })} + >{t("note_actions.export_as_image_svg")} + + ); +} diff --git a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx index 113d2ed14..b60abf93b 100644 --- a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx +++ b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx @@ -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 &&
    + + + + + + + +
    ); @@ -86,13 +116,13 @@ function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps ); } -function OpenExternallyButton({ note }: NoteActionsCustomInnerProps) { +function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) { return ( 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) && ); } + +function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, noteContext }: NoteActionsCustomInnerProps) { + const isEnabled = (note.noteId === "_backendLog" || noteType === "render") && isDefaultViewMode; + + return (isEnabled && + 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 && 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 && setReadOnly(!isReadOnly)} + />; +} + +function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) { + const isEnabled = noteMime.startsWith("application/javascript") || noteMime === "text/x-sqlite;schema=trilium"; + return isEnabled && ; +} + +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 && ; +} + +function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) { + const isEnabled = noteMime.startsWith("application/javascript;env="); + return isEnabled && openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} + />; +} + +function InAppHelpButton({ note, noteType }: NoteActionsCustomInnerProps) { + const helpUrl = getHelpUrlForNote(note); + const isEnabled = !!helpUrl && (noteType !== "book"); + + return isEnabled && ( + helpUrl && openInAppHelpFromUrl(helpUrl)} + /> + ); +} + +function AddChildButton({ parentComponent, noteType, viewType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) { + if (noteType === "book" && viewType === "geoMap") { + return parentComponent.triggerEvent("geoMapCreateChildNote", { ntxId })} + disabled={isReadOnly} + />; + } else if (noteType === "relationMap") { + return parentComponent.triggerEvent("relationMapCreateChildNote", { ntxId })} + disabled={isReadOnly} + />; + } +} //#endregion diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index eafcc7879..198929544 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -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; diff --git a/apps/client/src/widgets/type_widgets/code/BackendLog.tsx b/apps/client/src/widgets/type_widgets/code/BackendLog.tsx index f045ed562..64cc264c3 100644 --- a/apps/client/src/widgets/type_widgets/code/BackendLog.tsx +++ b/apps/client/src/widgets/type_widgets/code/BackendLog.tsx @@ -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(); @@ -40,5 +42,5 @@ export default function BackendLog({ ntxId, parentComponent }: TypeWidgetProps) preferPerformance />
    - ) + ); } diff --git a/apps/client/src/widgets/type_widgets/code/code.css b/apps/client/src/widgets/type_widgets/code/code.css index 1a4eea588..db5a9d2e8 100644 --- a/apps/client/src/widgets/type_widgets/code/code.css +++ b/apps/client/src/widgets/type_widgets/code/code.css @@ -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 */ \ No newline at end of file +/* #endregion */ diff --git a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css index 7ea0a003c..72f680b01 100644 --- a/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css +++ b/apps/client/src/widgets/type_widgets/helpers/SplitEditor.css @@ -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 */ @@ -93,4 +97,4 @@ height: 100%; max-width: 100%; } -/* #endregion */ \ No newline at end of file +/* #endregion */ diff --git a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx index 1ef0159c1..83a03153c 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx +++ b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx @@ -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(); const containerRef = useRef(null); const mapApiRef = useRef(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 ))} + + {isNewLayout && ( +
    + parentComponent?.triggerEvent("relationMapResetZoomIn", { ntxId })} + className="tn-tool-button" + noIconActionClass + /> + + parentComponent?.triggerEvent("relationMapResetZoomOut", { ntxId })} + className="tn-tool-button" + noIconActionClass + /> + + parentComponent?.triggerEvent("relationMapResetPanZoom", { ntxId })} + className="tn-tool-button" + noIconActionClass + /> +
    + )} ); } @@ -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) {