feat(react/bulk_actions): port add_label

This commit is contained in:
Elian Doran
2025-08-08 23:23:07 +03:00
parent 3dd6b05d2e
commit 6e1951b356
7 changed files with 122 additions and 99 deletions

View File

@@ -1,7 +1,41 @@
import { ComponentChildren } from "preact";
interface BulkActionProps {
label: string;
children: ComponentChildren;
helpText?: ComponentChildren;
}
export default function BulkAction() {
export default function BulkAction({ label, children, helpText }: BulkActionProps) {
return (
<tr>
<td colSpan={2}>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "10px" }} className="text-nowrap">{label}</div>
{children}
</div>
</td>
<td className="button-column">
{helpText && <div className="dropdown help-dropdown">
<span className="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div className="dropdown-menu dropdown-menu-right p-4">
{helpText}
</div>
</div>}
<span className="bx bx-x icon-action action-conf-del"></span>
</td>
</tr>
);
}
export function BulkActionText({ text }: { text: string }) {
return (
<div
style={{ marginRight: "10px", marginLeft: "10px" }}
className="text-nowrap">
{text}
</div>
);
}

View File

@@ -3,8 +3,9 @@ import server from "../../services/server.js";
import ws from "../../services/ws.js";
import utils from "../../services/utils.js";
import type FAttribute from "../../entities/fattribute.js";
import { VNode } from "preact";
interface ActionDefinition {
export interface ActionDefinition {
script: string;
relationName: string;
targetNoteId: string;
@@ -30,15 +31,18 @@ export default abstract class AbstractBulkAction {
render() {
try {
const $rendered = this.doRender();
if (Array.isArray($rendered)) {
$rendered
.find(".action-conf-del")
.on("click", () => this.deleteAction())
.attr("title", t("abstract_bulk_action.remove_this_search_action"));
$rendered
.find(".action-conf-del")
.on("click", () => this.deleteAction())
.attr("title", t("abstract_bulk_action.remove_this_search_action"));
utils.initHelpDropdown($rendered);
utils.initHelpDropdown($rendered);
return $rendered;
return $rendered;
} else {
return $rendered;
}
} catch (e: any) {
logError(`Failed rendering search action: ${JSON.stringify(this.attribute.dto)} with error: ${e.message} ${e.stack}`);
return null;
@@ -46,7 +50,7 @@ export default abstract class AbstractBulkAction {
}
// to be overridden
abstract doRender(): JQuery<HTMLElement>;
abstract doRender(): JQuery<HTMLElement> | VNode;
static get actionName() {
return "";
}

View File

@@ -1,70 +0,0 @@
import { t } from "../../../services/i18n.js";
import SpacedUpdate from "../../../services/spaced_update.js";
import AbstractBulkAction from "../abstract_bulk_action.js";
const TPL = /*html*/`
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px;" class="text-nowrap">${t("add_label.add_label")}</div>
<input type="text"
class="form-control label-name"
placeholder="${t("add_label.label_name_placeholder")}"
pattern="[\\p{L}\\p{N}_:]+"
title="${t("add_label.label_name_title")}"/>
<div style="margin-right: 10px; margin-left: 10px;" class="text-nowrap">${t("add_label.to_value")}</div>
<input type="text" class="form-control label-value" placeholder="${t("add_label.new_value_placeholder")}"/>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>${t("add_label.help_text")}</p>
<ul>
<li>${t("add_label.help_text_item1")}</li>
<li>${t("add_label.help_text_item2")}</li>
</ul>
${t("add_label.help_text_note")}
</div>
</div>
<span class="bx bx-x icon-action action-conf-del"></span>
</td>
</tr>`;
export default class AddLabelBulkAction extends AbstractBulkAction {
static get actionName() {
return "addLabel";
}
static get actionTitle() {
return t("add_label.add_label");
}
doRender() {
const $action = $(TPL);
const $labelName = $action.find(".label-name");
$labelName.val(this.actionDef.labelName || "");
const $labelValue = $action.find(".label-value");
$labelValue.val(this.actionDef.labelValue || "");
const spacedUpdate = new SpacedUpdate(async () => {
await this.saveAction({
labelName: $labelName.val(),
labelValue: $labelValue.val()
});
}, 1000);
$labelName.on("input", () => spacedUpdate.scheduleUpdate());
$labelValue.on("input", () => spacedUpdate.scheduleUpdate());
return $action;
}
}

View File

@@ -0,0 +1,56 @@
import { useEffect, useState } from "preact/hooks";
import { t } from "../../../services/i18n";
import FormTextBox from "../../react/FormTextBox";
import AbstractBulkAction, { ActionDefinition } from "../abstract_bulk_action";
import BulkAction, { BulkActionText } from "../BulkAction";
import { useSpacedUpdate } from "../../react/hooks";
function AddLabelBulkActionComponent({ bulkAction, actionDef }: { bulkAction: AbstractBulkAction, actionDef: ActionDefinition }) {
const [ labelName, setLabelName ] = useState<string>(actionDef.labelName ?? "");
const [ labelValue, setLabelValue ] = useState<string>(actionDef.labelValue ?? "");
const spacedUpdate = useSpacedUpdate(() => bulkAction.saveAction({ labelName, labelValue }));
useEffect(() => spacedUpdate.scheduleUpdate(), [labelName, labelValue]);
return (
<BulkAction
label={t("add_label.add_label")}
helpText={<>
<p>{t("add_label.help_text")}</p>
<ul>
<li>{t("add_label.help_text_item1")}</li>
<li>{t("add_label.help_text_item2")}</li>
</ul>
{t("add_label.help_text_note")}
</>}
>
<FormTextBox
placeholder={t("add_label.label_name_placeholder")}
pattern="[\\p{L}\\p{N}_:]+"
title={t("add_label.label_name_title")}
currentValue={labelName} onChange={setLabelName}
/>
<BulkActionText text={t("add_label.to_value")} />
<FormTextBox
placeholder={t("add_label.new_value_placeholder")}
currentValue={labelValue} onChange={setLabelValue}
/>
</BulkAction>
)
}
export default class AddLabelBulkAction extends AbstractBulkAction {
doRender() {
return <AddLabelBulkActionComponent bulkAction={this} actionDef={this.actionDef} />;
}
static get actionName() {
return "addLabel";
}
static get actionTitle() {
return t("add_label.add_label");
}
}

View File

@@ -104,16 +104,7 @@ function ExistingActionsList({ existingActions }: { existingActions?: RenameNote
<table class="bulk-existing-action-list">
{ existingActions
? existingActions
.map(action => {
const renderedAction = action.render();
if (renderedAction) {
return <RawHtmlBlock
html={renderedAction[0].innerHTML}
style={{ display: "flex", alignItems: "center" }} />
} else {
return null;
}
})
.map(action => action.render())
.filter(renderedAction => renderedAction !== null)
: <p>{t("bulk_actions.none_yet")}</p>
}

View File

@@ -1,17 +1,13 @@
import { HTMLInputTypeAttribute, RefObject } from "preact/compat";
import { InputHTMLAttributes, RefObject } from "preact/compat";
interface FormTextBoxProps {
interface FormTextBoxProps extends Pick<InputHTMLAttributes<HTMLInputElement>, "placeholder" | "autoComplete" | "className" | "type" | "name" | "pattern" | "title"> {
id?: string;
name: string;
type?: HTMLInputTypeAttribute;
currentValue?: string;
className?: string;
autoComplete?: string;
onChange?(newValue: string): void;
inputRef?: RefObject<HTMLInputElement>;
}
export default function FormTextBox({ id, type, name, className, currentValue, onChange, autoComplete, inputRef }: FormTextBoxProps) {
export default function FormTextBox({ id, type, name, className, currentValue, onChange, autoComplete, inputRef, placeholder, title, pattern }: FormTextBoxProps) {
return (
<input
ref={inputRef}
@@ -21,6 +17,9 @@ export default function FormTextBox({ id, type, name, className, currentValue, o
name={name}
value={currentValue}
autoComplete={autoComplete}
placeholder={placeholder}
title={title}
pattern={pattern}
onInput={e => onChange?.(e.currentTarget.value)} />
);
}

View File

@@ -1,6 +1,7 @@
import { useContext, useEffect, useState } from "preact/hooks";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { EventData, EventNames } from "../../components/app_context";
import { ParentComponent } from "./ReactBasicWidget";
import SpacedUpdate from "../../services/spaced_update";
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentWidget = useContext(ParentComponent);
@@ -29,4 +30,12 @@ export default function useTriliumEvent<T extends EventNames>(eventName: T, hand
parentWidget[handlerName] = originalHandler;
};
}, [parentWidget]);
}
export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) {
const spacedUpdate = useMemo(() => {
return new SpacedUpdate(callback, interval);
}, [callback, interval]);
return spacedUpdate;
}