feat(react/floating_buttons): port edit button

This commit is contained in:
Elian Doran 2025-08-27 22:33:36 +03:00
parent e290635ba5
commit cdbb89482e
No known key found for this signature in database
2 changed files with 63 additions and 92 deletions

View file

@ -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<boolean>;
}
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<FloatingButtonDefinition[]>(() => {
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<FloatingButtonDefinition[]>([]);
useEffect(() => {
if (!context) return;
getFloatingButtonDefinitions(context).then(setDefinitions);
}, [ context, refreshCounter, noteMime ]);
return (
<div className="floating-buttons no-print">
<div className="floating-buttons-children">
@ -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 <ActionButton
text={t("edit_button.edit_this_note")}
icon="bx bx-pencil"
className={animationClass}
onClick={() => {
if (noteContext.viewScope) {
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext });
}
}}
/>
}
/**
* Show button that displays floating button after click on close button
*/

View file

@ -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<void> {
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<void> {
if (this.isNote(noteId)) {
await this.refresh();
}
}
}