mirror of
https://github.com/zadam/trilium.git
synced 2025-12-16 21:29:56 +01:00
Merge branch 'main' into feat/restyle/backlinks-panel
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user