From 4fd02db0794f9879b99f08b0f22de15c6020087c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 28 Aug 2025 21:03:33 +0300 Subject: [PATCH 01/33] chore(react): remove irrelevant TODO --- apps/client/src/widgets/react/ActionButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index 41c4abbed..f12d6b286 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -5,7 +5,7 @@ import keyboard_actions from "../../services/keyboard_actions"; export interface ActionButtonProps { text: string; - titlePosition?: "bottom" | "left"; // TODO: Use it + titlePosition?: "bottom" | "left"; icon: string; className?: string; onClick?: (e: MouseEvent) => void; From fa66e5019302f71d075e89c168e24536e7dfcc83 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 28 Aug 2025 22:12:39 +0300 Subject: [PATCH 02/33] feat(react/widgets): port scroll padding --- apps/client/src/layouts/desktop_layout.tsx | 4 +-- apps/client/src/widgets/scroll_padding.ts | 33 ----------------- apps/client/src/widgets/scroll_padding.tsx | 42 ++++++++++++++++++++++ 3 files changed, 44 insertions(+), 35 deletions(-) delete mode 100644 apps/client/src/widgets/scroll_padding.ts create mode 100644 apps/client/src/widgets/scroll_padding.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index f9ad398e1..35de1a868 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -32,7 +32,7 @@ import LauncherContainer from "../widgets/containers/launcher_container.js"; import ApiLogWidget from "../widgets/api_log.js"; import MovePaneButton from "../widgets/buttons/move_pane_button.js"; import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js"; -import ScrollPaddingWidget from "../widgets/scroll_padding.js"; +import ScrollPadding from "../widgets/scroll_padding.js"; import options from "../services/options.js"; import utils from "../services/utils.js"; import CloseZenButton from "../widgets/close_zen_button.js"; @@ -141,7 +141,7 @@ export default class DesktopLayout { .child(new NoteListWidget(false)) .child(new SearchResultWidget()) .child(new SqlResultWidget()) - .child(new ScrollPaddingWidget()) + .child() ) .child(new ApiLogWidget()) .child(new FindWidget()) diff --git a/apps/client/src/widgets/scroll_padding.ts b/apps/client/src/widgets/scroll_padding.ts deleted file mode 100644 index 84c96c31b..000000000 --- a/apps/client/src/widgets/scroll_padding.ts +++ /dev/null @@ -1,33 +0,0 @@ -import NoteContextAwareWidget from "./note_context_aware_widget.js"; - -const TPL = /*html*/`
`; - -export default class ScrollPaddingWidget extends NoteContextAwareWidget { - - private $scrollingContainer!: JQuery; - - isEnabled() { - return super.isEnabled() && ["text", "code"].includes(this.note?.type ?? ""); - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$widget.on("click", () => this.triggerCommand("scrollToEnd", { ntxId: this.ntxId })); - } - - initialRenderCompleteEvent() { - this.$scrollingContainer = this.$widget.closest(".scrolling-container"); - - new ResizeObserver(() => this.refreshHeight()).observe(this.$scrollingContainer[0]); - - this.refreshHeight(); - } - - refreshHeight() { - const containerHeight = this.$scrollingContainer.height(); - - this.$widget.css("height", Math.round((containerHeight ?? 0) / 2)); - } -} diff --git a/apps/client/src/widgets/scroll_padding.tsx b/apps/client/src/widgets/scroll_padding.tsx new file mode 100644 index 000000000..d8452e301 --- /dev/null +++ b/apps/client/src/widgets/scroll_padding.tsx @@ -0,0 +1,42 @@ +import { useEffect, useRef, useState } from "preact/hooks"; +import { useNoteContext } from "./react/hooks"; + +export default function ScrollPadding() { + const { note, parentComponent, ntxId } = useNoteContext(); + const ref = useRef(null); + const [height, setHeight] = useState(10); + const isEnabled = ["text", "code"].includes(note?.type ?? ""); + + const refreshHeight = () => { + if (!ref.current) return; + const container = ref.current.closest(".scrolling-container") as HTMLElement | null; + if (!container) return; + setHeight(Math.round(container.offsetHeight / 2)); + }; + + useEffect(() => { + if (!isEnabled) return; + + const container = ref.current?.closest(".scrolling-container") as HTMLElement | null; + if (!container) return; + + // Observe container resize + const observer = new ResizeObserver(() => refreshHeight()); + observer.observe(container); + + // Initial resize + refreshHeight(); + + return () => observer.disconnect(); + }, [note]); // re-run when note changes + + return (isEnabled ? +
parentComponent.triggerCommand("scrollToEnd", { ntxId })} + /> + :
+ ) +} From 4ef103063d9edf7e9adc4aaf555e55df567b1b31 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 28 Aug 2025 23:13:24 +0300 Subject: [PATCH 03/33] feat(react/widgets): port search result --- apps/client/src/layouts/desktop_layout.tsx | 4 +- apps/client/src/widgets/react/Alert.tsx | 5 +- apps/client/src/widgets/search_result.css | 16 ++++ apps/client/src/widgets/search_result.ts | 89 ---------------------- apps/client/src/widgets/search_result.tsx | 63 +++++++++++++++ 5 files changed, 84 insertions(+), 93 deletions(-) create mode 100644 apps/client/src/widgets/search_result.css delete mode 100644 apps/client/src/widgets/search_result.ts create mode 100644 apps/client/src/widgets/search_result.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 35de1a868..79e8d2da4 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -11,7 +11,6 @@ import NoteListWidget from "../widgets/note_list.js"; import SqlResultWidget from "../widgets/sql_result.js"; import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js"; import NoteIconWidget from "../widgets/note_icon.jsx"; -import SearchResultWidget from "../widgets/search_result.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import RootContainer from "../widgets/containers/root_container.js"; import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js"; @@ -42,6 +41,7 @@ import { applyModals } from "./layout_commons.js"; import Ribbon from "../widgets/ribbon/Ribbon.jsx"; import FloatingButtons from "../widgets/FloatingButtons.jsx"; import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; +import SearchResult from "../widgets/search_result.jsx"; export default class DesktopLayout { @@ -139,7 +139,7 @@ export default class DesktopLayout { .child(new SqlTableSchemasWidget()) .child(new NoteDetailWidget()) .child(new NoteListWidget(false)) - .child(new SearchResultWidget()) + .child() .child(new SqlResultWidget()) .child() ) diff --git a/apps/client/src/widgets/react/Alert.tsx b/apps/client/src/widgets/react/Alert.tsx index 8b8afb68b..e57960157 100644 --- a/apps/client/src/widgets/react/Alert.tsx +++ b/apps/client/src/widgets/react/Alert.tsx @@ -4,11 +4,12 @@ interface AlertProps { type: "info" | "danger" | "warning"; title?: string; children: ComponentChildren; + className?: string; } -export default function Alert({ title, type, children }: AlertProps) { +export default function Alert({ title, type, children, className }: AlertProps) { return ( -
+
{title &&

{title}

} {children} diff --git a/apps/client/src/widgets/search_result.css b/apps/client/src/widgets/search_result.css new file mode 100644 index 000000000..5142bd776 --- /dev/null +++ b/apps/client/src/widgets/search_result.css @@ -0,0 +1,16 @@ +.search-result-widget { + flex-grow: 100000; + flex-shrink: 100000; + min-height: 0; + overflow: auto; + contain: none !important; +} + +.search-result-widget .note-list { + padding: 10px; +} + +.search-no-results, .search-not-executed-yet { + margin: 20px; + padding: 20px !important; +} \ No newline at end of file diff --git a/apps/client/src/widgets/search_result.ts b/apps/client/src/widgets/search_result.ts deleted file mode 100644 index 6fa69ae13..000000000 --- a/apps/client/src/widgets/search_result.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { t } from "../services/i18n.js"; -import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import NoteListRenderer from "../services/note_list_renderer.js"; -import type FNote from "../entities/fnote.js"; -import type { EventData } from "../components/app_context.js"; - -const TPL = /*html*/` -
- - -
- ${t("search_result.no_notes_found")} -
- -
- ${t("search_result.search_not_executed")} -
- -
-
-
`; - -export default class SearchResultWidget extends NoteContextAwareWidget { - - private $content!: JQuery; - private $noResults!: JQuery; - private $notExecutedYet!: JQuery; - - isEnabled() { - return super.isEnabled() && this.note?.type === "search"; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - this.$content = this.$widget.find(".search-result-widget-content"); - this.$noResults = this.$widget.find(".search-no-results"); - this.$notExecutedYet = this.$widget.find(".search-not-executed-yet"); - } - - async refreshWithNote(note: FNote) { - const noResults = note.getChildNoteIds().length === 0 && !!note.searchResultsLoaded; - - this.$content.empty(); - this.$noResults.toggle(noResults); - this.$notExecutedYet.toggle(!note.searchResultsLoaded); - - if (noResults || !note.searchResultsLoaded) { - return; - } - - const noteListRenderer = new NoteListRenderer({ - $parent: this.$content, - parentNote: note, - showNotePath: true - }); - await noteListRenderer.renderList(); - } - - searchRefreshedEvent({ ntxId }: EventData<"searchRefreshed">) { - if (!this.isNoteContext(ntxId)) { - return; - } - - this.refresh(); - } - - notesReloadedEvent({ noteIds }: EventData<"notesReloaded">) { - if (this.noteId && noteIds.includes(this.noteId)) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/search_result.tsx b/apps/client/src/widgets/search_result.tsx new file mode 100644 index 000000000..3392fbc69 --- /dev/null +++ b/apps/client/src/widgets/search_result.tsx @@ -0,0 +1,63 @@ +import { useEffect, useRef, useState } from "preact/hooks"; +import { t } from "../services/i18n"; +import Alert from "./react/Alert"; +import { useNoteContext, useNoteProperty, useTriliumEvent } from "./react/hooks"; +import "./search_result.css"; +import NoteListRenderer from "../services/note_list_renderer"; + +enum SearchResultState { + NO_RESULTS, + NOT_EXECUTED, + GOT_RESULTS +} + +export default function SearchResult() { + const { note, ntxId } = useNoteContext(); + const [ state, setState ] = useState(); + const searchContainerRef = useRef(null); + + function refresh() { + searchContainerRef.current?.replaceChildren(); + + if (!note?.searchResultsLoaded) { + setState(SearchResultState.NOT_EXECUTED); + } else if (note.getChildNoteIds().length === 0) { + setState(SearchResultState.NO_RESULTS); + } else if (searchContainerRef.current) { + setState(SearchResultState.GOT_RESULTS); + + const noteListRenderer = new NoteListRenderer({ + $parent: $(searchContainerRef.current), + parentNote: note, + showNotePath: true + }); + noteListRenderer.renderList(); + } + } + + useEffect(() => refresh(), [ note ]); + useTriliumEvent("searchRefreshed", ({ ntxId: eventNtxId }) => { + if (eventNtxId === ntxId) { + refresh(); + } + }); + useTriliumEvent("notesReloaded", ({ noteIds }) => { + if (note?.noteId && noteIds.includes(note.noteId)) { + refresh(); + } + }); + + return ( +
+ {state === SearchResultState.NOT_EXECUTED && ( + {t("search_result.search_not_executed")} + )} + + {state === SearchResultState.NO_RESULTS && ( + {t("search_result.no_notes_found")} + )} + +
+
+ ); +} \ No newline at end of file From 829f3827267cb01e1bc819d79c0ad3ac7e182a20 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 00:47:47 +0300 Subject: [PATCH 04/33] feat(react/widgets): global menu with zoom controls --- apps/client/src/components/app_context.ts | 1 + apps/client/src/layouts/desktop_layout.tsx | 10 +- apps/client/src/services/keyboard_actions.ts | 4 + .../src/widgets/buttons/global_menu.css | 81 ++++ .../client/src/widgets/buttons/global_menu.ts | 436 ------------------ .../src/widgets/buttons/global_menu.tsx | 133 ++++++ apps/client/src/widgets/react/Dropdown.tsx | 9 +- apps/client/src/widgets/react/FormList.tsx | 6 +- apps/client/src/widgets/react/hooks.tsx | 18 +- apps/client/src/widgets/ribbon/Ribbon.tsx | 13 +- 10 files changed, 255 insertions(+), 456 deletions(-) create mode 100644 apps/client/src/widgets/buttons/global_menu.css delete mode 100644 apps/client/src/widgets/buttons/global_menu.ts create mode 100644 apps/client/src/widgets/buttons/global_menu.tsx diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index ca4f9745f..2db2363a3 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -134,6 +134,7 @@ export type CommandMappings = { showLeftPane: CommandData; showAttachments: CommandData; showSearchHistory: CommandData; + showShareSubtree: CommandData; hoistNote: CommandData & { noteId: string }; leaveProtectedSession: CommandData; enterProtectedSession: CommandData; diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 79e8d2da4..1ad839b1a 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -1,5 +1,4 @@ import FlexContainer from "../widgets/containers/flex_container.js"; -import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; import TabRowWidget from "../widgets/tab_row.js"; import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js"; import LeftPaneContainer from "../widgets/containers/left_pane_container.js"; @@ -42,6 +41,7 @@ import Ribbon from "../widgets/ribbon/Ribbon.jsx"; import FloatingButtons from "../widgets/FloatingButtons.jsx"; import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; import SearchResult from "../widgets/search_result.jsx"; +import GlobalMenu from "../widgets/buttons/global_menu.jsx"; export default class DesktopLayout { @@ -176,12 +176,16 @@ export default class DesktopLayout { let launcherPane; if (isHorizontal) { - launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)); + launcherPane = new FlexContainer("row") + .css("height", "53px") + .class("horizontal") + .child(new LauncherContainer(true)) + .child(); } else { launcherPane = new FlexContainer("column") .css("width", "53px") .class("vertical") - .child(new GlobalMenuWidget(false)) + .child() .child(new LauncherContainer(false)) .child(new LeftPaneToggleWidget(false)); } diff --git a/apps/client/src/services/keyboard_actions.ts b/apps/client/src/services/keyboard_actions.ts index c72ba29bb..d65b10bb2 100644 --- a/apps/client/src/services/keyboard_actions.ts +++ b/apps/client/src/services/keyboard_actions.ts @@ -62,6 +62,10 @@ async function getAction(actionName: string, silent = false) { return action; } +export function getActionSync(actionName: string) { + return keyboardActionRepo[actionName]; +} + function updateDisplayedShortcuts($container: JQuery) { //@ts-ignore //TODO: each() does not support async callbacks. diff --git a/apps/client/src/widgets/buttons/global_menu.css b/apps/client/src/widgets/buttons/global_menu.css new file mode 100644 index 000000000..23bf1eae8 --- /dev/null +++ b/apps/client/src/widgets/buttons/global_menu.css @@ -0,0 +1,81 @@ +.global-menu { + width: 53px; + height: 53px; + flex-shrink: 0; +} + +.global-menu .dropdown-menu { + min-width: 20em; +} + +.global-menu-button { + width: 100% !important; + height: 100% !important; + position: relative; + padding: 6px; + border: 0; +} + +.global-menu-button svg path { + fill: var(--launcher-pane-text-color); +} + +.global-menu-button:hover { border: 0; } +.global-menu-button:hover svg path { + transition: 200ms ease-in-out fill; +} +.global-menu-button:hover svg path.st0 { fill:#95C980; } +.global-menu-button:hover svg path.st1 { fill:#72B755; } +.global-menu-button:hover svg path.st2 { fill:#4FA52B; } +.global-menu-button:hover svg path.st3 { fill:#EE8C89; } +.global-menu-button:hover svg path.st4 { fill:#E96562; } +.global-menu-button:hover svg path.st5 { fill:#E33F3B; } +.global-menu-button:hover svg path.st6 { fill:#EFB075; } +.global-menu-button:hover svg path.st7 { fill:#E99547; } +.global-menu-button:hover svg path.st8 { fill:#E47B19; } + +.global-menu-button-update-available { + position: absolute; + right: -30px; + bottom: -30px; + width: 100%; + height: 100%; + pointer-events: none; +} + +.global-menu .zoom-container { + display: flex; + flex-direction: row; + align-items: baseline; +} + +.global-menu .zoom-buttons { + margin-left: 2em; +} + +.global-menu .zoom-buttons a { + display: inline-block; + border: 1px solid var(--button-border-color); + border-radius: var(--button-border-radius); + color: var(--button-text-color); + background-color: var(--button-background-color); + padding: 3px; + margin-left: 3px; + text-decoration: none; +} + +.global-menu .zoom-buttons a:hover { + text-decoration: none; +} + +.global-menu .zoom-state { + margin-left: 5px; + margin-right: 5px; +} + +.global-menu .dropdown-item .bx { + position: relative; + top: 3px; + font-size: 120%; + margin-right: 6px; +} \ No newline at end of file diff --git a/apps/client/src/widgets/buttons/global_menu.ts b/apps/client/src/widgets/buttons/global_menu.ts deleted file mode 100644 index 9d6493b89..000000000 --- a/apps/client/src/widgets/buttons/global_menu.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { t } from "../../services/i18n.js"; -import BasicWidget from "../basic_widget.js"; -import utils from "../../services/utils.js"; -import UpdateAvailableWidget from "./update_available.js"; -import options from "../../services/options.js"; -import { Tooltip, Dropdown } from "bootstrap"; - -const TPL = /*html*/` - -`; - -export default class GlobalMenuWidget extends BasicWidget { - private updateAvailableWidget: UpdateAvailableWidget; - private isHorizontalLayout: boolean; - private tooltip!: Tooltip; - private dropdown!: Dropdown; - - private $updateToLatestVersionButton!: JQuery; - private $zoomState!: JQuery; - private $toggleZenMode!: JQuery; - - constructor(isHorizontalLayout: boolean) { - super(); - - this.updateAvailableWidget = new UpdateAvailableWidget(); - this.isHorizontalLayout = isHorizontalLayout; - } - - doRender() { - this.$widget = $(TPL); - - if (!this.isHorizontalLayout) { - this.$widget.addClass("dropend"); - } - - const $globalMenuButton = this.$widget.find(".global-menu-button"); - if (!this.isHorizontalLayout) { - $globalMenuButton.prepend( - $(`\ - - - - - - - - - - - - - - - `) - ); - - this.tooltip = new Tooltip(this.$widget.find("[data-bs-toggle='tooltip']")[0], { trigger: "hover" }); - } else { - $globalMenuButton.toggleClass("bx bx-menu"); - } - - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], { - popperConfig: { - placement: "bottom" - } - }); - - this.$widget.find(".show-about-dialog-button").on("click", () => this.triggerCommand("openAboutDialog")); - - const isElectron = utils.isElectron(); - - this.$widget.find(".toggle-pin").toggle(isElectron); - if (isElectron) { - this.$widget.on("click", ".toggle-pin", (e) => { - const $el = $(e.target); - const remote = utils.dynamicRequire("@electron/remote"); - const focusedWindow = remote.BrowserWindow.getFocusedWindow(); - const isAlwaysOnTop = focusedWindow.isAlwaysOnTop(); - if (isAlwaysOnTop) { - focusedWindow.setAlwaysOnTop(false); - $el.removeClass("active"); - } else { - focusedWindow.setAlwaysOnTop(true); - $el.addClass("active"); - } - }); - } - - this.$widget.find(".logout-button").toggle(!isElectron); - this.$widget.find(".logout-button-separator").toggle(!isElectron); - - this.$widget.find(".open-dev-tools-button").toggle(isElectron); - this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop()); - this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile()); - - this.$widget.on("click", ".dropdown-item", (e) => { - if ($(e.target).parent(".zoom-buttons")) { - return; - } - - this.dropdown.toggle(); - }); - if (utils.isMobile()) { - this.$widget.on("click", ".dropdown-submenu .dropdown-toggle", (e) => { - const $submenu = $(e.target).closest(".dropdown-item"); - $submenu.toggleClass("submenu-open"); - $submenu.find("ul.dropdown-menu").toggleClass("show"); - e.stopPropagation(); - return; - }); - } - this.$widget.on("click", ".dropdown-submenu", (e) => { - if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass("dropdown-toggle")) { - e.stopPropagation(); - } - }); - - this.$widget.find(".global-menu-button-update-available").append(this.updateAvailableWidget.render()); - - this.$updateToLatestVersionButton = this.$widget.find(".update-to-latest-version-button"); - - if (!utils.isElectron()) { - this.$widget.find(".zoom-container").hide(); - } - - this.$zoomState = this.$widget.find(".zoom-state"); - this.$toggleZenMode = this.$widget.find('[data-trigger-command="toggleZenMode"'); - this.$widget.on("show.bs.dropdown", () => this.#onShown()); - if (this.tooltip) { - this.$widget.on("hide.bs.dropdown", () => this.tooltip.enable()); - } - - this.$widget.find(".zoom-buttons").on( - "click", - // delay to wait for the actual zoom change - () => setTimeout(() => this.updateZoomState(), 300) - ); - - this.updateVersionStatus(); - - setInterval(() => this.updateVersionStatus(), 8 * 60 * 60 * 1000); - } - - #onShown() { - this.$toggleZenMode.toggleClass("active", $("body").hasClass("zen")); - this.updateZoomState(); - if (this.tooltip) { - this.tooltip.hide(); - this.tooltip.disable(); - } - } - - updateZoomState() { - if (!utils.isElectron()) { - return; - } - - const zoomFactor = utils.dynamicRequire("electron").webFrame.getZoomFactor(); - const zoomPercent = Math.round(zoomFactor * 100); - - this.$zoomState.text(`${zoomPercent}%`); - } - - async updateVersionStatus() { - await options.initializedPromise; - - if (options.get("checkForUpdates") !== "true") { - return; - } - - const latestVersion = await this.fetchLatestVersion(); - this.updateAvailableWidget.updateVersionStatus(latestVersion); - // Show "click to download" button in options menu if there's a new version available - this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion)); - this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`); - } - - async fetchLatestVersion() { - const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest"; - - const resp = await fetch(RELEASES_API_URL); - const data = await resp.json(); - - return data?.tag_name?.substring(1); - } - - downloadLatestVersionCommand() { - window.open("https://github.com/TriliumNext/Trilium/releases/latest"); - } - - activeContextChangedEvent() { - this.dropdown.hide(); - } - - noteSwitchedEvent() { - this.dropdown.hide(); - } -} diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx new file mode 100644 index 000000000..344572c1c --- /dev/null +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -0,0 +1,133 @@ +import Dropdown from "../react/Dropdown"; +import "./global_menu.css"; +import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut } from "../react/hooks"; +import { useContext, useEffect, useRef, useState } from "preact/hooks"; +import { t } from "../../services/i18n"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { CommandNames } from "../../components/app_context"; +import KeyboardShortcut from "../react/KeyboardShortcut"; +import { KeyboardActionNames } from "@triliumnext/commons"; +import { ComponentChildren } from "preact"; +import Component from "../../components/component"; +import { ParentComponent } from "../react/react_utils"; +import { dynamicRequire, isElectron } from "../../services/utils"; + +interface MenuItemProps { + icon: string, + text: ComponentChildren, + title?: string, + command: T, + disabled?: boolean +} + +export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: boolean }) { + const isVerticalLayout = !isHorizontalLayout; + const parentComponent = useContext(ParentComponent); + + return ( + } + forceShown + > + + + + + + + + + ) +} + +function MenuItem({ icon, text, title, command, disabled }: MenuItemProps void)>) { + return {text} +} + +function KeyboardAction({ text, command, ...props }: MenuItemProps) { + return {text} } + /> +} + +function VerticalLayoutIcon() { + const logoRef = useRef(null); + useStaticTooltip(logoRef); + + return ( + + + + + + + + + + + + + + + + ) +} + +function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) { + const [ zoomLevel, setZoomLevel ] = useState(100); + + function updateZoomState() { + if (!isElectron()) { + return; + } + + const zoomFactor = dynamicRequire("electron").webFrame.getZoomFactor(); + setZoomLevel(Math.round(zoomFactor * 100)); + } + + useEffect(updateZoomState, []); + + function ZoomControlButton({ command, title, icon, children }: { command: KeyboardActionNames, title: string, icon?: string, children?: ComponentChildren }) { + const linkRef = useRef(null); + useStaticTooltipWithKeyboardShortcut(linkRef, title, command); + return ( + { + parentComponent?.triggerCommand(command); + setTimeout(() => updateZoomState(), 300) + e.stopPropagation(); + }} + className={icon} + >{children} + ) + } + + return isElectron() ? ( + +
+ +   + + {zoomLevel}{t("units.percentage")} + +
+ } + >{t("global_menu.zoom")}
+ ) : ( + + ); +} diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index fbcd6a78e..91403b622 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -18,9 +18,10 @@ export interface DropdownProps { noSelectButtonStyle?: boolean; disabled?: boolean; text?: ComponentChildren; + forceShown?: boolean; } -export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle }: DropdownProps) { +export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, forceShown }: DropdownProps) { const dropdownRef = useRef(null); const triggerRef = useRef(null); @@ -30,8 +31,12 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre if (!triggerRef.current) return; const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current); + if (forceShown) { + dropdown.show(); + setShown(true); + } return () => dropdown.dispose(); - }, []); // Add dependency array + }, []); const onShown = useCallback(() => { setShown(true); diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index c12c8a317..5e3bae3cf 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -87,16 +87,17 @@ interface FormListItemOpts { description?: string; className?: string; rtl?: boolean; + outsideChildren?: ComponentChildren; } -export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand }: FormListItemOpts) { +export function FormListItem({ className, children, icon, value, title, active, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand, outsideChildren }: FormListItemOpts) { if (checked) { icon = "bx bx-check"; } return ( {description}
}
+ {outsideChildren} ); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index af5378144..bd0c2e287 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -2,7 +2,7 @@ import { useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, use import { EventData, EventNames } from "../../components/app_context"; import { ParentComponent } from "./react_utils"; import SpacedUpdate from "../../services/spaced_update"; -import { OptionNames } from "@triliumnext/commons"; +import { KeyboardActionNames, OptionNames } from "@triliumnext/commons"; import options, { type OptionValue } from "../../services/options"; import utils, { reloadFrontendApp } from "../../services/utils"; import NoteContext from "../../components/note_context"; @@ -14,6 +14,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget"; import { RefObject, VNode } from "preact"; import { Tooltip } from "bootstrap"; import { CSSProperties } from "preact/compat"; +import keyboard_actions from "../../services/keyboard_actions"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { const parentComponent = useContext(ParentComponent); @@ -502,7 +503,7 @@ export function useTooltip(elRef: RefObject, config: Partial, config?: Partial) { +export function useStaticTooltip(elRef: RefObject, config?: Partial) { useEffect(() => { if (!elRef?.current) return; @@ -514,6 +515,19 @@ export function useStaticTooltip(elRef: RefObject, config?: Partial }, [ elRef, config ]); } +export function useStaticTooltipWithKeyboardShortcut(elRef: RefObject, title: string, actionName: KeyboardActionNames) { + const [ keyboardShortcut, setKeyboardShortcut ] = useState(); + useStaticTooltip(elRef, { + title: keyboardShortcut?.length ? `${title} (${keyboardShortcut?.join(",")})` : title + }); + + useEffect(() => { + if (actionName) { + keyboard_actions.getAction(actionName).then(action => setKeyboardShortcut(action?.effectiveShortcuts)); + } + }, [actionName]); +} + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function useLegacyImperativeHandlers(handlers: Record) { const parentComponent = useContext(ParentComponent); diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index 53813b1f4..4c1b6a12b 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { t } from "../../services/i18n"; -import { useNoteContext, useNoteProperty, useStaticTooltip, useTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks"; +import { useNoteContext, useNoteProperty, useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks"; import "./style.css"; import { VNode } from "preact"; import BasicPropertiesTab from "./BasicPropertiesTab"; @@ -252,16 +252,7 @@ export default function Ribbon() { function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) { const iconRef = useRef(null); - const [ keyboardShortcut, setKeyboardShortcut ] = useState(); - useStaticTooltip(iconRef, { - title: keyboardShortcut?.length ? `${title} (${keyboardShortcut?.join(",")})` : title - }); - - useEffect(() => { - if (toggleCommand) { - keyboard_actions.getAction(toggleCommand).then(action => setKeyboardShortcut(action?.effectiveShortcuts)); - } - }, [toggleCommand]); + useStaticTooltipWithKeyboardShortcut(iconRef, title, toggleCommand); return ( <> From e166b97b8f7ccba57eb55495f849fb9dd1aec500 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 01:07:11 +0300 Subject: [PATCH 05/33] feat(react/widgets): port a few more global menu items --- apps/client/src/layouts/mobile_layout.tsx | 7 +++- .../src/widgets/buttons/global_menu.tsx | 40 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index 18c3d4a2e..e80c605fe 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -162,7 +162,12 @@ export default class MobileLayout { .contentSized() .id("mobile-bottom-bar") .child(new TabRowWidget().css("height", "40px")) - .child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane")) + .child(new FlexContainer("row") + .class("horizontal") + .css("height", "53px") + .child(new LauncherContainer(true)) + .child() + .id("launcher-pane")) ) .child(new CloseZenButton()); applyModals(rootContainer); diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 344572c1c..5d07aec66 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -10,7 +10,7 @@ import { KeyboardActionNames } from "@triliumnext/commons"; import { ComponentChildren } from "preact"; import Component from "../../components/component"; import { ParentComponent } from "../react/react_utils"; -import { dynamicRequire, isElectron } from "../../services/utils"; +import { dynamicRequire, isElectron, isMobile } from "../../services/utils"; interface MenuItemProps { icon: string, @@ -18,6 +18,8 @@ interface MenuItemProps { title?: string, command: T, disabled?: boolean + active?: boolean; + outsideChildren?: ComponentChildren; } export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: boolean }) { @@ -34,21 +36,32 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: - + + + + + {isMobile() ? ( + + ) : ( + + )} + ) } -function MenuItem({ icon, text, title, command, disabled }: MenuItemProps void)>) { +function MenuItem({ icon, text, title, command, disabled, active, outsideChildren }: MenuItemProps void)>) { return {text} } @@ -56,7 +69,8 @@ function KeyboardAction({ text, command, ...props }: MenuItemProps{text} } + text={text} + outsideChildren={} /> } @@ -131,3 +145,21 @@ function ZoomControls({ parentComponent }: { parentComponent?: Component | null ); } + +function ToggleWindowOnTop() { + const focusedWindow = isElectron() ? dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow() : null; + const [ isAlwaysOnTop, setIsAlwaysOnTop ] = useState(focusedWindow?.isAlwaysOnTop()); + + return (isElectron() && + { + const newState = !isAlwaysOnTop; + focusedWindow?.setAlwaysOnTop(newState); + setIsAlwaysOnTop(newState); + }} + /> + ) +} \ No newline at end of file From 83fd42aff29156a68f5588fb93ee2df98fdd9797 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 11:54:16 +0300 Subject: [PATCH 06/33] feat(react): add bootstrap tooltip to menu items --- apps/client/src/widgets/react/FormList.tsx | 28 +++++++++++++++++++++- apps/client/src/widgets/react/hooks.tsx | 3 ++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index 5e3bae3cf..7b44ad69d 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -1,9 +1,10 @@ -import { Dropdown as BootstrapDropdown } from "bootstrap"; +import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap"; import { ComponentChildren } from "preact"; import Icon from "./Icon"; import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat"; import "./FormList.css"; import { CommandNames } from "../../components/app_context"; +import { useStaticTooltip } from "./hooks"; interface FormListOpts { children: ComponentChildren; @@ -90,13 +91,23 @@ interface FormListItemOpts { outsideChildren?: ComponentChildren; } +const TOOLTIP_CONFIG: Partial = { + placement: "right", + fallbackPlacements: [ "right" ] +} + export function FormListItem({ className, children, icon, value, title, active, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand, outsideChildren }: FormListItemOpts) { + const itemRef = useRef(null); + if (checked) { icon = "bx bx-check"; } + useStaticTooltip(itemRef, TOOLTIP_CONFIG); + return ( ; +} + +export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) { + return ( +
  • + + + {title} + + +
      + {children} +
    +
  • + ) } \ No newline at end of file diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index bd0c2e287..58670155a 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -505,7 +505,8 @@ export function useTooltip(elRef: RefObject, config: Partial, config?: Partial) { useEffect(() => { - if (!elRef?.current) return; + const hasTooltip = config?.title || elRef.current?.getAttribute("title"); + if (!elRef?.current || !hasTooltip) return; const $el = $(elRef.current); $el.tooltip(config); From dbbae87cd33ad87e032553b2e3c7dad09e683588 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 11:55:05 +0300 Subject: [PATCH 07/33] feat(react/global_menu): port advanced options --- .../src/widgets/buttons/global_menu.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 5d07aec66..ec784b21c 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -3,7 +3,7 @@ import "./global_menu.css"; import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut } from "../react/hooks"; import { useContext, useEffect, useRef, useState } from "preact/hooks"; import { t } from "../../services/i18n"; -import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList"; import { CommandNames } from "../../components/app_context"; import KeyboardShortcut from "../react/KeyboardShortcut"; import { KeyboardActionNames } from "@triliumnext/commons"; @@ -40,7 +40,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: - + {isMobile() ? ( @@ -49,10 +49,29 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: )} + ) } +function AdvancedMenu() { + return ( + + + + + + + + + + + + + + ) +} + function MenuItem({ icon, text, title, command, disabled, active, outsideChildren }: MenuItemProps void)>) { return {text} } -function KeyboardAction({ text, command, ...props }: MenuItemProps) { +function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps) { return Date: Fri, 29 Aug 2025 12:00:45 +0300 Subject: [PATCH 08/33] feat(react/global_menu): add a few more items --- apps/client/src/widgets/buttons/global_menu.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index ec784b21c..40bea197c 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -50,6 +50,15 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: )} + + + + + + + + + ) } From 70f826b737f0e616c0f4da33a683b78d40e97d07 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 12:30:22 +0300 Subject: [PATCH 09/33] feat(react/global_menu): add update indicator --- .../src/widgets/buttons/global_menu.css | 23 +++++++++- .../src/widgets/buttons/global_menu.tsx | 42 +++++++++++++++++-- .../src/widgets/buttons/update_available.ts | 40 ------------------ 3 files changed, 61 insertions(+), 44 deletions(-) delete mode 100644 apps/client/src/widgets/buttons/update_available.ts diff --git a/apps/client/src/widgets/buttons/global_menu.css b/apps/client/src/widgets/buttons/global_menu.css index 23bf1eae8..920308dea 100644 --- a/apps/client/src/widgets/buttons/global_menu.css +++ b/apps/client/src/widgets/buttons/global_menu.css @@ -78,4 +78,25 @@ top: 3px; font-size: 120%; margin-right: 6px; -} \ No newline at end of file +} + +/* #region Update available */ +.global-menu-button-update-available-button { + width: 21px !important; + height: 21px !important; + padding: 0 !important; + + border-radius: var(--button-border-radius); + transform: scale(0.9); + border: none; + opacity: 0.8; + + display: flex; + align-items: center; + justify-content: center; +} + +.global-menu-button-wrapper:hover .global-menu-button-update-available-button { + opacity: 1; +} +/* #endregion */ \ No newline at end of file diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 40bea197c..09747d9f7 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -1,6 +1,6 @@ import Dropdown from "../react/Dropdown"; import "./global_menu.css"; -import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut } from "../react/hooks"; +import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool } from "../react/hooks"; import { useContext, useEffect, useRef, useState } from "preact/hooks"; import { t } from "../../services/i18n"; import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList"; @@ -10,7 +10,7 @@ import { KeyboardActionNames } from "@triliumnext/commons"; import { ComponentChildren } from "preact"; import Component from "../../components/component"; import { ParentComponent } from "../react/react_utils"; -import { dynamicRequire, isElectron, isMobile } from "../../services/utils"; +import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils"; interface MenuItemProps { icon: string, @@ -25,12 +25,18 @@ interface MenuItemProps { export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: boolean }) { const isVerticalLayout = !isHorizontalLayout; const parentComponent = useContext(ParentComponent); + const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus(); return ( } + text={<> + {isVerticalLayout && } + {isUpdateAvailable && } + } forceShown > @@ -56,6 +62,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: + {isUpdateAvailable && window.open("https://github.com/TriliumNext/Trilium/releases/latest")} icon="bx bx-sync" text={`Version ${latestVersion} is available, click to download.`} /> } @@ -190,4 +197,33 @@ function ToggleWindowOnTop() { }} /> ) +} + +function useTriliumUpdateStatus() { + const [ latestVersion, setLatestVersion ] = useState(); + const [ checkForUpdates ] = useTriliumOptionBool("checkForUpdates"); + const isUpdateAvailable = utils.isUpdateAvailable(latestVersion, glob.triliumVersion); + + async function updateVersionStatus() { + const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest"; + + const resp = await fetch(RELEASES_API_URL); + const data = await resp.json(); + const latestVersion = data?.tag_name?.substring(1); + setLatestVersion(latestVersion); + } + + useEffect(() => { + if (!checkForUpdates) { + setLatestVersion(undefined); + return; + } + + updateVersionStatus(); + + const interval = setInterval(() => updateVersionStatus(), 8 * 60 * 60 * 1000); + return () => clearInterval(interval); + }, [ checkForUpdates ]); + + return { isUpdateAvailable, latestVersion }; } \ No newline at end of file diff --git a/apps/client/src/widgets/buttons/update_available.ts b/apps/client/src/widgets/buttons/update_available.ts deleted file mode 100644 index 2f2535cc0..000000000 --- a/apps/client/src/widgets/buttons/update_available.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { t } from "../../services/i18n.js"; -import BasicWidget from "../basic_widget.js"; -import utils from "../../services/utils.js"; - -const TPL = /*html*/` -
    - - - -
    -`; - -export default class UpdateAvailableWidget extends BasicWidget { - doRender() { - this.$widget = $(TPL); - } - - updateVersionStatus(latestVersion: string) { - this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion)); - } -} From 0d1bd3e2984c9a98f80e6672e7ddde19e8d580ec Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 12:36:12 +0300 Subject: [PATCH 10/33] feat(react/global_menu): add show/hide conditions --- .../src/widgets/buttons/global_menu.tsx | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 09747d9f7..5b3cfe862 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -48,12 +48,8 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: - - {isMobile() ? ( - - ) : ( - - )} + + @@ -63,9 +59,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: {isUpdateAvailable && window.open("https://github.com/TriliumNext/Trilium/releases/latest")} icon="bx bx-sync" text={`Version ${latestVersion} is available, click to download.`} /> } - - - + {!isElectron() && }
    ) } @@ -82,12 +76,29 @@ function AdvancedMenu() { - + {isElectron() && } ) } +function BrowserOnlyOptions() { + return <> + + + ; +} + +function SwitchToOptions() { + if (isElectron()) { + return; + } else if (!isMobile()) { + return + } else { + return + } +} + function MenuItem({ icon, text, title, command, disabled, active, outsideChildren }: MenuItemProps void)>) { return Date: Fri, 29 Aug 2025 12:40:16 +0300 Subject: [PATCH 11/33] chore(react/global_menu): advanced submenu toggle on mobile --- apps/client/src/widgets/react/FormList.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index 7b44ad69d..fb5318631 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -1,10 +1,11 @@ import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap"; import { ComponentChildren } from "preact"; import Icon from "./Icon"; -import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat"; +import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact/compat"; import "./FormList.css"; import { CommandNames } from "../../components/app_context"; import { useStaticTooltip } from "./hooks"; +import { isMobile } from "../../services/utils"; interface FormListOpts { children: ComponentChildren; @@ -145,14 +146,25 @@ export function FormDropdownDivider() { } export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) { + const [ openOnMobile, setOpenOnMobile ] = useState(false); + return ( -
  • - +
  • + { + e.stopPropagation(); + + if (isMobile()) { + setOpenOnMobile(!openOnMobile); + } + }} + > {title} -
      +
        {children}
      From 168ff90e389b288951274aa85f6e0b11d92fa6cd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 12:48:10 +0300 Subject: [PATCH 12/33] fix(react/global_menu): menu layout on mobile --- apps/client/src/widgets/react/Dropdown.tsx | 4 +-- apps/client/src/widgets/react/FormList.tsx | 32 ++++++++++++++-------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index 91403b622..08cdbfed0 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -80,13 +80,13 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre -
      {shown && children} -
      +
  • ) } \ No newline at end of file diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index fb5318631..8991dea00 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -97,8 +97,8 @@ const TOOLTIP_CONFIG: Partial = { fallbackPlacements: [ "right" ] } -export function FormListItem({ className, children, icon, value, title, active, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand, outsideChildren }: FormListItemOpts) { - const itemRef = useRef(null); +export function FormListItem({ className, icon, value, title, active, disabled, checked, onClick, selected, rtl, triggerCommand, outsideChildren, description, ...contentProps }: FormListItemOpts) { + const itemRef = useRef(null); if (checked) { icon = "bx bx-check"; @@ -107,7 +107,7 @@ export function FormListItem({ className, children, icon, value, title, active, useStaticTooltip(itemRef, TOOLTIP_CONFIG); return ( -   -
    - {children} - {badges && badges.map(({ className, text }) => ( - {text} - ))} - {description &&
    {description}
    } -
    + {description ? ( +
    + +
    + ) : ( + + )} {outsideChildren} -
    + ); } +function FormListContent({ children, badges, description }: Pick) { + return <> + {children} + {badges && badges.map(({ className, text }) => ( + {text} + ))} + {description &&
    {description}
    } + ; +} + interface FormListHeaderOpts { text: string; } From f0ac301417b6c1342afc6b5486f846b2b465914b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 12:50:45 +0300 Subject: [PATCH 13/33] refactor(react/global_menu): get rid of outsideChildren --- apps/client/src/widgets/buttons/global_menu.tsx | 14 +++++++------- apps/client/src/widgets/react/FormList.tsx | 4 +--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 5b3cfe862..b6e850726 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -99,7 +99,7 @@ function SwitchToOptions() { } } -function MenuItem({ icon, text, title, command, disabled, active, outsideChildren }: MenuItemProps void)>) { +function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps void)>) { return {text} } @@ -115,8 +114,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps} + text={<>{text} } /> } @@ -177,7 +175,9 @@ function ZoomControls({ parentComponent }: { parentComponent?: Component | null + > + {t("global_menu.zoom")} + <>
      @@ -185,8 +185,8 @@ function ZoomControls({ parentComponent }: { parentComponent?: Component | null {zoomLevel}{t("units.percentage")}
    - } - >{t("global_menu.zoom")}
    + + ) : ( ); diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index 8991dea00..eca945061 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -89,7 +89,6 @@ interface FormListItemOpts { description?: string; className?: string; rtl?: boolean; - outsideChildren?: ComponentChildren; } const TOOLTIP_CONFIG: Partial = { @@ -97,7 +96,7 @@ const TOOLTIP_CONFIG: Partial = { fallbackPlacements: [ "right" ] } -export function FormListItem({ className, icon, value, title, active, disabled, checked, onClick, selected, rtl, triggerCommand, outsideChildren, description, ...contentProps }: FormListItemOpts) { +export function FormListItem({ className, icon, value, title, active, disabled, checked, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) { const itemRef = useRef(null); if (checked) { @@ -124,7 +123,6 @@ export function FormListItem({ className, icon, value, title, active, disabled, ) : ( )} - {outsideChildren} ); } From e49e2d50939548c5e347ce6b3660df373bfe97f0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 13:01:54 +0300 Subject: [PATCH 14/33] fix(react/global_menu): styling and layout of keyboard shortcuts --- apps/client/src/stylesheets/style.css | 6 ++++- .../src/stylesheets/theme-next/base.css | 6 ++++- .../src/widgets/react/KeyboardShortcut.tsx | 22 +++++++++---------- apps/client/src/widgets/react/react_utils.tsx | 6 +++-- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index ae6e2109a..d22d633b7 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -437,14 +437,18 @@ body #context-menu-container .dropdown-item > span { align-items: center; } -.dropdown-menu kbd { +.dropdown-item span.keyboard-shortcut { flex-grow: 1; text-align: right; +} + +.dropdown-menu kbd { color: var(--muted-text-color); border: none; background-color: transparent; box-shadow: none; padding-bottom: 0; + padding: 0; } .dropdown-item, diff --git a/apps/client/src/stylesheets/theme-next/base.css b/apps/client/src/stylesheets/theme-next/base.css index bee48c023..524bb8140 100644 --- a/apps/client/src/stylesheets/theme-next/base.css +++ b/apps/client/src/stylesheets/theme-next/base.css @@ -197,13 +197,17 @@ html body .dropdown-item[disabled] { /* Menu item keyboard shortcut */ .dropdown-item kbd { - margin-left: 16px; font-family: unset !important; font-size: unset !important; color: var(--menu-item-keyboard-shortcut-color) !important; padding-top: 0; } +.dropdown-item span.keyboard-shortcut { + color: var(--menu-item-keyboard-shortcut-color) !important; + margin-left: 16px; +} + .dropdown-divider { position: relative; border-color: transparent !important; diff --git a/apps/client/src/widgets/react/KeyboardShortcut.tsx b/apps/client/src/widgets/react/KeyboardShortcut.tsx index ca8f6a852..0a76b8093 100644 --- a/apps/client/src/widgets/react/KeyboardShortcut.tsx +++ b/apps/client/src/widgets/react/KeyboardShortcut.tsx @@ -2,11 +2,14 @@ import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/common import { useEffect, useState } from "preact/hooks"; import keyboard_actions from "../../services/keyboard_actions"; import { joinElements } from "./react_utils"; +import utils from "../../services/utils"; interface KeyboardShortcutProps { actionName: KeyboardActionNames; } +const isMobile = utils.isMobile(); + export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps) { const [ action, setAction ] = useState(); @@ -18,17 +21,14 @@ export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps) return <>; } - return ( - <> - {action.effectiveShortcuts?.map((shortcut) => { + return (!isMobile && + + {joinElements(action.effectiveShortcuts?.map((shortcut) => { const keys = shortcut.split("+"); - return joinElements(keys - .map((key, i) => ( - <> - {key} {i + 1 < keys.length && "+ "} - - ))) - })} - + return joinElements( + keys.map((key, i) => {key}) + , "+"); + }))} + ); } \ No newline at end of file diff --git a/apps/client/src/widgets/react/react_utils.tsx b/apps/client/src/widgets/react/react_utils.tsx index 40bb1b9cb..5e436bf14 100644 --- a/apps/client/src/widgets/react/react_utils.tsx +++ b/apps/client/src/widgets/react/react_utils.tsx @@ -41,7 +41,9 @@ export function disposeReactWidget(container: Element) { render(null, container); } -export function joinElements(components: ComponentChild[], separator = ", ") { +export function joinElements(components: ComponentChild[] | undefined, separator = ", ") { + if (!components) return <>; + const joinedComponents: ComponentChild[] = []; for (let i=0; i{joinedComponents}; } From 70440520e1aa2aebb6be897ee37f7223e1f44794 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 15:01:29 +0300 Subject: [PATCH 15/33] fix(react/global_menu): misalignment of the "advanced" submenu --- apps/client/src/widgets/react/FormList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index eca945061..f3d6d9427 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -168,7 +168,7 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t } }} > - + {" "} {title} From 4df94d1f2094f236ff488bb6d9e4aa57108a9d86 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 15:02:56 +0300 Subject: [PATCH 16/33] chore(react/global_menu): add missing command names --- apps/client/src/components/app_context.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 2db2363a3..42f270176 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -89,6 +89,11 @@ export type CommandMappings = { closeTocCommand: CommandData; closeHlt: CommandData; showLaunchBarSubtree: CommandData; + showHiddenSubtree: CommandData; + showSQLConsoleHistory: CommandData; + logout: CommandData; + switchToMobileVersion: CommandData; + switchToDesktopVersion: CommandData; showRevisions: CommandData & { noteId?: string | null; }; From 735e91e6368a6714192030e210cd7b4f3db00de1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 15:50:44 +0300 Subject: [PATCH 17/33] feat(react/widgets): port sql_result --- apps/client/src/components/entrypoints.ts | 16 +--- apps/client/src/layouts/desktop_layout.tsx | 3 +- apps/client/src/widgets/sql_result.css | 7 ++ apps/client/src/widgets/sql_result.ts | 88 ---------------------- apps/client/src/widgets/sql_result.tsx | 62 +++++++++++++++ apps/server/src/routes/api/notes.ts | 4 +- packages/commons/src/lib/server_api.ts | 16 +++- 7 files changed, 89 insertions(+), 107 deletions(-) create mode 100644 apps/client/src/widgets/sql_result.css delete mode 100644 apps/client/src/widgets/sql_result.ts create mode 100644 apps/client/src/widgets/sql_result.tsx diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index 2e55a9b9d..6f8aefc24 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -11,21 +11,7 @@ import froca from "../services/froca.js"; import linkService from "../services/link.js"; import { t } from "../services/i18n.js"; import type FNote from "../entities/fnote.js"; - -// TODO: Move somewhere else nicer. -export type SqlExecuteResults = string[][][]; - -// TODO: Deduplicate with server. -interface SqlExecuteResponse { - success: boolean; - error?: string; - results: SqlExecuteResults; -} - -// TODO: Deduplicate with server. -interface CreateChildrenResponse { - note: FNote; -} +import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; export default class Entrypoints extends Component { constructor() { diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 1ad839b1a..96d7c09ee 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -42,6 +42,7 @@ import FloatingButtons from "../widgets/FloatingButtons.jsx"; import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; import SearchResult from "../widgets/search_result.jsx"; import GlobalMenu from "../widgets/buttons/global_menu.jsx"; +import SqlResults from "../widgets/sql_result.js"; export default class DesktopLayout { @@ -140,7 +141,7 @@ export default class DesktopLayout { .child(new NoteDetailWidget()) .child(new NoteListWidget(false)) .child() - .child(new SqlResultWidget()) + .child() .child() ) .child(new ApiLogWidget()) diff --git a/apps/client/src/widgets/sql_result.css b/apps/client/src/widgets/sql_result.css new file mode 100644 index 000000000..63b5621ed --- /dev/null +++ b/apps/client/src/widgets/sql_result.css @@ -0,0 +1,7 @@ +.sql-result-widget { + padding: 15px; +} + +.sql-console-result-container td { + white-space: preserve; +} \ No newline at end of file diff --git a/apps/client/src/widgets/sql_result.ts b/apps/client/src/widgets/sql_result.ts deleted file mode 100644 index f9f579315..000000000 --- a/apps/client/src/widgets/sql_result.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { EventData } from "../components/app_context.js"; -import { t } from "../services/i18n.js"; -import NoteContextAwareWidget from "./note_context_aware_widget.js"; - -const TPL = /*html*/` -
    - - - - -
    -
    `; - -export default class SqlResultWidget extends NoteContextAwareWidget { - - private $resultContainer!: JQuery; - private $noRowsAlert!: JQuery; - - isEnabled() { - return this.note && this.note.mime === "text/x-sqlite;schema=trilium" && super.isEnabled(); - } - - doRender() { - this.$widget = $(TPL); - - this.$resultContainer = this.$widget.find(".sql-console-result-container"); - this.$noRowsAlert = this.$widget.find(".sql-query-no-rows"); - } - - async sqlQueryResultsEvent({ ntxId, results }: EventData<"sqlQueryResults">) { - if (!this.isNoteContext(ntxId)) { - return; - } - - this.$noRowsAlert.toggle(results.length === 1 && results[0].length === 0); - this.$resultContainer.toggle(results.length > 1 || results[0].length > 0); - - this.$resultContainer.empty(); - - for (const rows of results) { - if (typeof rows === "object" && !Array.isArray(rows)) { - // inserts, updates - this.$resultContainer - .empty() - .show() - .append($("
    ").text(JSON.stringify(rows, null, "\t")));
    -
    -                continue;
    -            }
    -
    -            if (!rows.length) {
    -                continue;
    -            }
    -
    -            const $table = $('');
    -            this.$resultContainer.append($table);
    -
    -            const result = rows[0];
    -            const $row = $("");
    -
    -            for (const key in result) {
    -                $row.append($("");
    -
    -                for (const key in result) {
    -                    $row.append($("
    ").text(key)); - } - - $table.append($row); - - for (const result of rows) { - const $row = $("
    ").text(result[key])); - } - - $table.append($row); - } - } - } -} diff --git a/apps/client/src/widgets/sql_result.tsx b/apps/client/src/widgets/sql_result.tsx new file mode 100644 index 000000000..760651774 --- /dev/null +++ b/apps/client/src/widgets/sql_result.tsx @@ -0,0 +1,62 @@ +import { SqlExecuteResults } from "@triliumnext/commons"; +import { useNoteContext, useTriliumEvent } from "./react/hooks"; +import "./sql_result.css"; +import { useState } from "preact/hooks"; +import Alert from "./react/Alert"; +import { t } from "../services/i18n"; + +export default function SqlResults() { + const { note, ntxId } = useNoteContext(); + const [ results, setResults ] = useState(); + + useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => { + if (eventNtxId !== ntxId) return; + setResults(results); + }) + + return ( +
    + {note?.mime === "text/x-sqlite;schema=trilium" && ( + results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? ( + + {t("sql_result.no_rows")} + + ) : ( +
    + {results?.map(rows => { + // inserts, updates + if (typeof rows === "object" && !Array.isArray(rows)) { + return
    {JSON.stringify(rows, null, "\t")}
    + } + + // selects + return + })} +
    + ) + )} +
    + ) +} + +function SqlResultTable({ rows }: { rows: object[] }) { + if (!rows.length) return; + + return ( + + + + {Object.keys(rows[0]).map(key => )} + + + + + {rows.map(row => ( + + {Object.values(row).map(cell => )} + + ))} + +
    {key}
    {cell}
    + ) +} \ No newline at end of file diff --git a/apps/server/src/routes/api/notes.ts b/apps/server/src/routes/api/notes.ts index f481d199a..8a426dea3 100644 --- a/apps/server/src/routes/api/notes.ts +++ b/apps/server/src/routes/api/notes.ts @@ -12,7 +12,7 @@ import ValidationError from "../../errors/validation_error.js"; import blobService from "../../services/blob.js"; import type { Request } from "express"; import type BBranch from "../../becca/entities/bbranch.js"; -import type { AttributeRow, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons"; +import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons"; /** * @swagger @@ -123,7 +123,7 @@ function createNote(req: Request) { return { note, branch - }; + } satisfies CreateChildrenResponse; } function updateNoteData(req: Request) { diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index cadbcfe0c..bf712a6e4 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -1,4 +1,4 @@ -import { AttachmentRow, AttributeRow, NoteType } from "./rows.js"; +import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from "./rows.js"; type Response = { success: true, @@ -220,3 +220,17 @@ export type BacklinksResponse = ({ noteId: string; excerpts: string[] })[]; + + +export type SqlExecuteResults = (object[] | object)[]; + +export interface SqlExecuteResponse { + success: boolean; + error?: string; + results: SqlExecuteResults; +} + +export interface CreateChildrenResponse { + note: NoteRow; + branch: BranchRow; +} From f2ce8b9f3c0f37a87f50ca26c7246c7e46358d94 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 16:28:49 +0300 Subject: [PATCH 18/33] feat(react/widgets): search results interfering with SQL results + bad note path style --- apps/client/src/widgets/search_result.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/search_result.tsx b/apps/client/src/widgets/search_result.tsx index 3392fbc69..88ca424fa 100644 --- a/apps/client/src/widgets/search_result.tsx +++ b/apps/client/src/widgets/search_result.tsx @@ -5,7 +5,7 @@ import { useNoteContext, useNoteProperty, useTriliumEvent } from "./react/hooks" import "./search_result.css"; import NoteListRenderer from "../services/note_list_renderer"; -enum SearchResultState { +enum SearchResultState { NO_RESULTS, NOT_EXECUTED, GOT_RESULTS @@ -19,7 +19,9 @@ export default function SearchResult() { function refresh() { searchContainerRef.current?.replaceChildren(); - if (!note?.searchResultsLoaded) { + if (note?.type !== "search") { + setState(undefined); + } else if (!note?.searchResultsLoaded) { setState(SearchResultState.NOT_EXECUTED); } else if (note.getChildNoteIds().length === 0) { setState(SearchResultState.NO_RESULTS); @@ -57,7 +59,7 @@ export default function SearchResult() { {t("search_result.no_notes_found")} )} -
    +
    ); } \ No newline at end of file From 753f1dc7b6979475a55b02d6d4934d272690ed8b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 29 Aug 2025 17:14:27 +0300 Subject: [PATCH 19/33] feat(react/widgets): sql table schemas --- apps/client/src/layouts/desktop_layout.tsx | 5 +- .../src/stylesheets/theme-next/pages.css | 13 --- apps/client/src/widgets/sql_table_schemas.css | 43 +++++++++ apps/client/src/widgets/sql_table_schemas.ts | 94 ------------------- apps/client/src/widgets/sql_table_schemas.tsx | 45 +++++++++ apps/server/src/routes/api/sql.ts | 2 +- packages/commons/src/lib/server_api.ts | 8 ++ 7 files changed, 99 insertions(+), 111 deletions(-) create mode 100644 apps/client/src/widgets/sql_table_schemas.css delete mode 100644 apps/client/src/widgets/sql_table_schemas.ts create mode 100644 apps/client/src/widgets/sql_table_schemas.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 96d7c09ee..f689f1082 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -7,8 +7,6 @@ import NoteTitleWidget from "../widgets/note_title.jsx"; import NoteDetailWidget from "../widgets/note_detail.js"; import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import NoteListWidget from "../widgets/note_list.js"; -import SqlResultWidget from "../widgets/sql_result.js"; -import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js"; import NoteIconWidget from "../widgets/note_icon.jsx"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import RootContainer from "../widgets/containers/root_container.js"; @@ -43,6 +41,7 @@ import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions. import SearchResult from "../widgets/search_result.jsx"; import GlobalMenu from "../widgets/buttons/global_menu.jsx"; import SqlResults from "../widgets/sql_result.js"; +import SqlTableSchemas from "../widgets/sql_table_schemas.js"; export default class DesktopLayout { @@ -137,7 +136,7 @@ export default class DesktopLayout { new ScrollingContainer() .filling() .child(new PromotedAttributesWidget()) - .child(new SqlTableSchemasWidget()) + .child() .child(new NoteDetailWidget()) .child(new NoteListWidget(false)) .child() diff --git a/apps/client/src/stylesheets/theme-next/pages.css b/apps/client/src/stylesheets/theme-next/pages.css index 93cc30589..9816f21fb 100644 --- a/apps/client/src/stylesheets/theme-next/pages.css +++ b/apps/client/src/stylesheets/theme-next/pages.css @@ -96,7 +96,6 @@ background: var(--background) !important; color: var(--color) !important; line-height: unset; - cursor: help; } .sql-table-schemas-widget .sql-table-schemas button:hover, @@ -106,18 +105,6 @@ --color: var(--main-text-color); } -/* Tooltip */ - -.tooltip .table-schema { - font-family: var(--monospace-font-family); - font-size: .85em; -} - -/* Data type */ -.tooltip .table-schema td:nth-child(2) { - color: var(--muted-text-color); -} - /* * NOTE MAP */ diff --git a/apps/client/src/widgets/sql_table_schemas.css b/apps/client/src/widgets/sql_table_schemas.css new file mode 100644 index 000000000..d13a71dfd --- /dev/null +++ b/apps/client/src/widgets/sql_table_schemas.css @@ -0,0 +1,43 @@ +.sql-table-schemas-widget { + padding: 12px; + padding-right: 10%; + contain: none !important; +} + +.sql-table-schemas > .dropdown { + display: inline-block !important; +} + +.sql-table-schemas button.btn { + padding: 0.25rem 0.4rem; + font-size: 0.875rem; + line-height: 0.5; + border: 1px solid var(--button-border-color); + border-radius: var(--button-border-radius); + background: var(--button-background-color); + color: var(--button-text-color); + cursor: pointer; +} + +.sql-console-result-container { + width: 100%; + font-size: smaller; + margin-top: 10px; + flex-grow: 1; + overflow: auto; + min-height: 0; +} + +.table-schema td { + padding: 5px; +} + +.dropdown .table-schema { + font-family: var(--monospace-font-family); + font-size: .85em; +} + +/* Data type */ +.dropdown .table-schema td:nth-child(2) { + color: var(--muted-text-color); +} \ No newline at end of file diff --git a/apps/client/src/widgets/sql_table_schemas.ts b/apps/client/src/widgets/sql_table_schemas.ts deleted file mode 100644 index 5a15881c4..000000000 --- a/apps/client/src/widgets/sql_table_schemas.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { t } from "../services/i18n.js"; -import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import server from "../services/server.js"; -import type FNote from "../entities/fnote.js"; - -const TPL = /*html*/` -
    - - - ${t("sql_table_schemas.tables")}: - -
    `; - -interface SchemaResponse { - name: string; - columns: { - name: string; - type: string; - }[]; -} - -export default class SqlTableSchemasWidget extends NoteContextAwareWidget { - - private tableSchemasShown?: boolean; - private $sqlConsoleTableSchemas!: JQuery; - - isEnabled() { - return this.note && this.note.mime === "text/x-sqlite;schema=trilium" && super.isEnabled(); - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$sqlConsoleTableSchemas = this.$widget.find(".sql-table-schemas"); - } - - async refreshWithNote(note: FNote) { - if (this.tableSchemasShown) { - return; - } - - this.tableSchemasShown = true; - - const tableSchema = await server.get("sql/schema"); - - for (const table of tableSchema) { - const $tableLink = $('