From 0c0504ffd1c97e5309dae0b32c728d933b5664a6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 28 Aug 2025 19:56:53 +0300 Subject: [PATCH] refactor(react/floating_buttons): use enabled at component level --- apps/client/src/widgets/FloatingButtons.tsx | 55 +---- .../widgets/FloatingButtonsDefinitions.tsx | 215 ++++++++---------- 2 files changed, 112 insertions(+), 158 deletions(-) diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 4d8c7e46f..08d026fba 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -1,22 +1,12 @@ import { t } from "i18next"; import "./FloatingButtons.css"; -import { useNoteContext, useNoteProperty, useTriliumEvent, useTriliumEvents } from "./react/hooks"; +import { useNoteContext, useNoteLabel, useNoteLabelBoolean } from "./react/hooks"; import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { ParentComponent } from "./react/react_utils"; -import attributes from "../services/attributes"; import { EventData, EventNames } from "../components/app_context"; -import { FLOATING_BUTTON_DEFINITIONS, FloatingButtonContext, FloatingButtonDefinition } from "./FloatingButtonsDefinitions"; +import { FLOATING_BUTTONS, type FloatingButtonContext } from "./FloatingButtonsDefinitions"; import ActionButton from "./react/ActionButton"; - -async function getFloatingButtonDefinitions(context: FloatingButtonContext) { - const defs: FloatingButtonDefinition[] = []; - for (const def of FLOATING_BUTTON_DEFINITIONS) { - if (await def.isEnabled(context)) { - defs.push(def); - } - } - return defs; -} +import { ViewTypeOptions } from "../services/note_list_renderer"; /* * Note: @@ -28,6 +18,8 @@ async function getFloatingButtonDefinitions(context: FloatingButtonContext) { export default function FloatingButtons() { const { note, noteContext } = useNoteContext(); const parentComponent = useContext(ParentComponent); + const [ viewType ] = useNoteLabel(note, "viewType"); + const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly"); const context = useMemo(() => { if (!note || !noteContext || !parentComponent) return null; @@ -35,6 +27,9 @@ export default function FloatingButtons() { note, noteContext, parentComponent, + isDefaultViewMode: noteContext.viewScope?.viewMode === "default", + viewType: viewType as ViewTypeOptions, + isReadOnly, triggerEvent(name: T, data?: Omit, "ntxId">) { parentComponent.triggerEvent(name, { ntxId: noteContext.ntxId, @@ -42,33 +37,7 @@ export default function FloatingButtons() { } as EventData); } }; - }, [ note, noteContext, parentComponent ]); - - // Refresh on any note attribute change. - const [ refreshCounter, setRefreshCounter ] = useState(0); - useTriliumEvent("entitiesReloaded", ({ loadResults }) => { - if (loadResults.getAttributeRows().find(attrRow => attributes.isAffecting(attrRow, note))) { - setRefreshCounter(refreshCounter + 1); - } - }); - useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { - if (noteContext?.ntxId === eventNoteContext.ntxId) { - setRefreshCounter(refreshCounter + 1); - } - }); - useTriliumEvents(["reEvaluateTocWidgetVisibility", "reEvaluateHighlightsListWidgetVisibility"], ({ noteId }) => { - if (noteId === note?.noteId) { - setRefreshCounter(refreshCounter + 1); - } - }); - - // Manage the list of items - const noteMime = useNoteProperty(note, "mime"); - const [ definitions, setDefinitions ] = useState([]); - useEffect(() => { - if (!context) return; - getFloatingButtonDefinitions(context).then(setDefinitions); - }, [ context, refreshCounter, noteMime ]); + }, [ note, noteContext, parentComponent, viewType, isReadOnly ]); // Manage the user-adjustable visibility of the floating buttons. const [ visible, setVisible ] = useState(true); @@ -77,14 +46,14 @@ export default function FloatingButtons() { return (
- {context && definitions.map(({ component: Component }) => ( + {context && FLOATING_BUTTONS.map((Component) => ( ))} - {definitions.length && } + {visible && }
- {!visible && definitions.length && } + {!visible && }
) } diff --git a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx index 96caf9b60..05e47fd93 100644 --- a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx +++ b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx @@ -4,7 +4,7 @@ import Component from "../components/component"; import NoteContext from "../components/note_context"; import FNote from "../entities/fnote"; import ActionButton, { ActionButtonProps } from "./react/ActionButton"; -import { useNoteLabelBoolean, useTriliumOption, useWindowSize } from "./react/hooks"; +import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils"; import server from "../services/server"; @@ -19,18 +19,17 @@ import { getHelpUrlForNote } from "../services/in_app_help"; import froca from "../services/froca"; import NoteLink from "./react/NoteLink"; import RawHtml from "./react/RawHtml"; - -export interface FloatingButtonDefinition { - component: (context: FloatingButtonContext) => VNode; - isEnabled: (context: FloatingButtonContext) => boolean | Promise; -} +import { ViewTypeOptions } from "../services/note_list_renderer"; export interface FloatingButtonContext { parentComponent: Component; note: FNote; noteContext: NoteContext; + isDefaultViewMode: boolean; + isReadOnly: boolean; /** Shorthand for triggering an event from the parent component. The `ntxId` is automatically handled for convenience. */ triggerEvent(name: T, data?: Omit, "ntxId">): void; + viewType?: ViewTypeOptions | null; } function FloatingButton({ className, ...props }: ActionButtonProps) { @@ -41,126 +40,90 @@ function FloatingButton({ className, ...props }: ActionButtonProps) { /> } -export const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ - { - component: RefreshBackendLogButton, - isEnabled: ({ note, noteContext }) => note.noteId === "_backendLog" && noteContext.viewScope?.viewMode === "default", - }, - { - component: SwitchSplitOrientationButton, - isEnabled: ({ note, noteContext }) => note.type === "mermaid" && note.isContentAvailable() && !note.hasLabel("readOnly") && noteContext.viewScope?.viewMode === "default" - }, - { - component: ToggleReadOnlyButton, - isEnabled: ({ note, noteContext }) => - (note.type === "mermaid" || note.getLabelValue("viewType") === "geoMap") - && note.isContentAvailable() - && noteContext.viewScope?.viewMode === "default" - }, - { - component: EditButton, - isEnabled: async ({ note, noteContext }) => - noteContext.viewScope?.viewMode === "default" - && (!note.isProtected || protected_session_holder.isProtectedSessionAvailable()) - && !options.is("databaseReadonly") - && await noteContext?.isReadOnly() - }, - { - component: ShowTocWidgetButton, - isEnabled: ({ note, noteContext }) => - note.type === "text" && noteContext?.viewScope?.viewMode === "default" - && !!noteContext.viewScope?.tocTemporarilyHidden - }, - { - component: ShowHighlightsListWidgetButton, - isEnabled: ({ note, noteContext }) => - note.type === "text" && noteContext?.viewScope?.viewMode === "default" - && !!noteContext.viewScope?.highlightsListTemporarilyHidden - }, - { - component: RunActiveNoteButton, - isEnabled: ({ note }) => note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium" - }, - { - component: OpenTriliumApiDocsButton, - isEnabled: ({ note }) => note.mime.startsWith("application/javascript;env=") - }, - { - component: SaveToNoteButton, - isEnabled: ({ note }) => note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely() - }, - { - component: RelationMapButtons, - isEnabled: ({ note }) => note.type === "relationMap" - }, - { - component: GeoMapButtons, - isEnabled: ({ note }) => note?.getLabelValue("viewType") === "geoMap" && !note.hasLabel("readOnly") - }, - { - component: CopyImageReferenceButton, - isEnabled: ({ note, noteContext }) => - ["mermaid", "canvas", "mindMap"].includes(note?.type ?? "") - && note?.isContentAvailable() && noteContext.viewScope?.viewMode === "default" - }, - { - component: ExportImageButtons, - isEnabled: ({ note, noteContext }) => - ["mermaid", "mindMap"].includes(note?.type ?? "") - && note?.isContentAvailable() && noteContext?.viewScope?.viewMode === "default" - }, - { - component: InAppHelpButton, - isEnabled: ({ note }) => !!getHelpUrlForNote(note) - }, - { - component: Backlinks, - isEnabled: ({ noteContext }) => noteContext.viewScope?.viewMode === "default" - } +export const FLOATING_BUTTONS: ((context: FloatingButtonContext) => false | VNode)[] = [ + RefreshBackendLogButton, + SwitchSplitOrientationButton, + ToggleReadOnlyButton, + EditButton, + ShowTocWidgetButton, + ShowHighlightsListWidgetButton, + RunActiveNoteButton, + OpenTriliumApiDocsButton, + SaveToNoteButton, + RelationMapButtons, + GeoMapButtons, + CopyImageReferenceButton, + ExportImageButtons, + InAppHelpButton, + Backlinks ]; -function RefreshBackendLogButton({ parentComponent, noteContext }: FloatingButtonContext) { - return parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} /> } -function SwitchSplitOrientationButton({ }: FloatingButtonContext) { +function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) { + const isEnabled = note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode; const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation"); const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal"; - return setSplitEditorOrientation(upcomingOrientation)} /> } -function ToggleReadOnlyButton({ note }: FloatingButtonContext) { - const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); +function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) { + const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); + const isEnabled = (note.type === "mermaid" || viewType === "geoMap") + && note.isContentAvailable() && isDefaultViewMode; - return setReadOnly(!isReadOnly)} /> } -function EditButton({ noteContext }: FloatingButtonContext) { +function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { const [ animationClass, setAnimationClass ] = useState(""); + const [ isEnabled, setIsEnabled ] = useState(false); + + useEffect(() => { + noteContext.isReadOnly().then(isReadOnly => { + setIsEnabled( + isDefaultViewMode + && (!note.isProtected || protected_session_holder.isProtectedSessionAvailable()) + && !options.is("databaseReadonly") + && isReadOnly + ); + }); + }, [ note ]); + + useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { + if (noteContext?.ntxId === eventNoteContext.ntxId) { + setIsEnabled(false); + } + }); // make the edit button stand out on the first display, otherwise // it's difficult to notice that the note is readonly useEffect(() => { - setAnimationClass("bx-tada bx-lg"); - setTimeout(() => { - setAnimationClass(""); - }, 1700); - }, []); + if (isEnabled) { + setAnimationClass("bx-tada bx-lg"); + setTimeout(() => { + setAnimationClass(""); + }, 1700); + } + }, [ isEnabled ]); - return } -function ShowTocWidgetButton({ noteContext }: FloatingButtonContext) { - return { + setIsEnabled(note.type === "text" && isDefaultViewMode && !!noteContext.viewScope?.tocTemporarilyHidden); + }); + + return isEnabled && { @@ -186,8 +154,13 @@ function ShowTocWidgetButton({ noteContext }: FloatingButtonContext) { /> } -function ShowHighlightsListWidgetButton({ noteContext }: FloatingButtonContext) { - return { + setIsEnabled(note.type === "text" && isDefaultViewMode && !!noteContext.viewScope?.highlightsListTemporarilyHidden); + }); + + return isEnabled && { @@ -199,8 +172,9 @@ function ShowHighlightsListWidgetButton({ noteContext }: FloatingButtonContext) /> } -function RunActiveNoteButton() { - return openInAppHelpFromUrl(note.mime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} @@ -216,7 +191,8 @@ function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) { } function SaveToNoteButton({ note }: FloatingButtonContext) { - return { @@ -232,8 +208,9 @@ function SaveToNoteButton({ note }: FloatingButtonContext) { /> } -function RelationMapButtons({ triggerEvent }: FloatingButtonContext) { - return ( +function RelationMapButtons({ note, triggerEvent }: FloatingButtonContext) { + const isEnabled = (note.type === "relationMap"); + return isEnabled && ( <> (null); + const isEnabled = ["mermaid", "canvas", "mindMap"].includes(note?.type ?? "") + && note?.isContentAvailable() && isDefaultViewMode; - return ( + return isEnabled && ( <> (null); useEffect(() => { + if (!isDefaultViewMode) return; + server.get(`note-map/${note.noteId}/backlink-count`).then(resp => { setBacklinkCount(resp.count); }); @@ -351,7 +335,8 @@ function Backlinks({ note }: FloatingButtonContext) { } }, [ popupOpen, windowHeight ]); - return (backlinkCount > 0 && + const isEnabled = isDefaultViewMode && backlinkCount > 0; + return (isEnabled &&