From cdbb89482e0b5f90fada15a7c9f14e874a2dc892 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Aug 2025 22:33:36 +0300 Subject: [PATCH] feat(react/floating_buttons): port edit button --- apps/client/src/widgets/FloatingButtons.tsx | 71 ++++++++++++++-- .../widgets/floating_buttons/edit_button.ts | 84 ------------------- 2 files changed, 63 insertions(+), 92 deletions(-) delete mode 100644 apps/client/src/widgets/floating_buttons/edit_button.ts diff --git a/apps/client/src/widgets/FloatingButtons.tsx b/apps/client/src/widgets/FloatingButtons.tsx index 60ef54a74..358ae2c62 100644 --- a/apps/client/src/widgets/FloatingButtons.tsx +++ b/apps/client/src/widgets/FloatingButtons.tsx @@ -4,12 +4,16 @@ import Button from "./react/Button"; import ActionButton from "./react/ActionButton"; import FNote from "../entities/fnote"; import NoteContext from "../components/note_context"; -import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "./react/hooks"; +import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption, useTriliumOptionBool } from "./react/hooks"; import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { ParentComponent } from "./react/react_utils"; import Component from "../components/component"; import { VNode } from "preact"; import attributes from "../services/attributes"; +import appContext from "../components/app_context"; +import protected_session_holder from "../services/protected_session_holder"; +import options from "../services/options"; +import { AttributeRow } from "../services/load_results"; interface FloatingButtonContext { parentComponent: Component; @@ -19,7 +23,7 @@ interface FloatingButtonContext { interface FloatingButtonDefinition { component: (context: FloatingButtonContext) => VNode; - isEnabled: (context: FloatingButtonContext) => boolean; + isEnabled: (context: FloatingButtonContext) => boolean | Promise; } const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ @@ -37,9 +41,27 @@ const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [ (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() } ]; +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; +} + /* * Note: * @@ -64,15 +86,23 @@ export default function FloatingButtons() { const [ refreshCounter, setRefreshCounter ] = useState(0); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (loadResults.getAttributeRows().find(attrRow => attributes.isAffecting(attrRow, note))) { - setRefreshCounter(refreshCounter+1); + setRefreshCounter(refreshCounter + 1); + } + }); + useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { + if (noteContext?.ntxId === eventNoteContext.ntxId) { + setRefreshCounter(refreshCounter + 1); } }); - - const definitions = useMemo(() => { - if (!context) return []; - return FLOATING_BUTTON_DEFINITIONS.filter(def => def.isEnabled(context)); - }, [ context, refreshCounter ]); + // 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 ]); + return (
@@ -115,6 +145,31 @@ function ToggleReadOnlyButton({ note }: FloatingButtonContext) { /> } +function EditButton({ noteContext }: FloatingButtonContext) { + const [ animationClass, setAnimationClass ] = useState(""); + + // 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); + }, []); + + return { + if (noteContext.viewScope) { + noteContext.viewScope.readOnlyTemporarilyDisabled = true; + appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext }); + } + }} + /> +} + /** * Show button that displays floating button after click on close button */ diff --git a/apps/client/src/widgets/floating_buttons/edit_button.ts b/apps/client/src/widgets/floating_buttons/edit_button.ts deleted file mode 100644 index 344447f31..000000000 --- a/apps/client/src/widgets/floating_buttons/edit_button.ts +++ /dev/null @@ -1,84 +0,0 @@ -import OnClickButtonWidget from "../buttons/onclick_button.js"; -import appContext from "../../components/app_context.js"; -import attributeService from "../../services/attributes.js"; -import protectedSessionHolder from "../../services/protected_session_holder.js"; -import { t } from "../../services/i18n.js"; -import LoadResults from "../../services/load_results.js"; -import type { AttributeRow } from "../../services/load_results.js"; -import FNote from "../../entities/fnote.js"; -import options from "../../services/options.js"; - -export default class EditButton extends OnClickButtonWidget { - isEnabled(): boolean { - return Boolean(super.isEnabled() && this.note && this.noteContext?.viewScope?.viewMode === "default"); - } - - constructor() { - super(); - - this.icon("bx-pencil") - .title(t("edit_button.edit_this_note")) - .titlePlacement("bottom") - .onClick((widget) => { - if (this.noteContext?.viewScope) { - this.noteContext.viewScope.readOnlyTemporarilyDisabled = true; - appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext: this.noteContext }); - } - }); - } - - async refreshWithNote(note: FNote): Promise { - if (options.is("databaseReadonly")) { - this.toggleInt(false); - return; - } - if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { - this.toggleInt(false); - } else { - // prevent flickering by assuming hidden before async operation - this.toggleInt(false); - - const wasVisible = this.isVisible(); - - // can't do this in isEnabled() since isReadOnly is async - const isReadOnly = await this.noteContext?.isReadOnly(); - this.toggleInt(Boolean(isReadOnly)); - - // make the edit button stand out on the first display, otherwise - // it's difficult to notice that the note is readonly - if (this.isVisible() && !wasVisible && this.$widget) { - this.$widget.addClass("bx-tada bx-lg"); - - setTimeout(() => { - this.$widget?.removeClass("bx-tada bx-lg"); - }, 1700); - } - } - - await super.refreshWithNote(note); - } - - entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): void { - if (loadResults.getAttributeRows().find((attr: AttributeRow) => - attr.type === "label" && - attr.name?.toLowerCase().includes("readonly") && - this.note && - attributeService.isAffecting(attr, this.note) - )) { - if (this.noteContext?.viewScope) { - this.noteContext.viewScope.readOnlyTemporarilyDisabled = false; - } - this.refresh(); - } - } - - readOnlyTemporarilyDisabledEvent() { - this.refresh(); - } - - async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise { - if (this.isNote(noteId)) { - await this.refresh(); - } - } -}