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 { 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 ws from "../../services/ws.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import type FAttribute from "../../entities/fattribute.js"; import type FAttribute from "../../entities/fattribute.js";
import { VNode } from "preact";
interface ActionDefinition { export interface ActionDefinition {
script: string; script: string;
relationName: string; relationName: string;
targetNoteId: string; targetNoteId: string;
@@ -30,7 +31,7 @@ export default abstract class AbstractBulkAction {
render() { render() {
try { try {
const $rendered = this.doRender(); const $rendered = this.doRender();
if (Array.isArray($rendered)) {
$rendered $rendered
.find(".action-conf-del") .find(".action-conf-del")
.on("click", () => this.deleteAction()) .on("click", () => this.deleteAction())
@@ -39,6 +40,9 @@ export default abstract class AbstractBulkAction {
utils.initHelpDropdown($rendered); utils.initHelpDropdown($rendered);
return $rendered; return $rendered;
} else {
return $rendered;
}
} catch (e: any) { } catch (e: any) {
logError(`Failed rendering search action: ${JSON.stringify(this.attribute.dto)} with error: ${e.message} ${e.stack}`); logError(`Failed rendering search action: ${JSON.stringify(this.attribute.dto)} with error: ${e.message} ${e.stack}`);
return null; return null;
@@ -46,7 +50,7 @@ export default abstract class AbstractBulkAction {
} }
// to be overridden // to be overridden
abstract doRender(): JQuery<HTMLElement>; abstract doRender(): JQuery<HTMLElement> | VNode;
static get actionName() { static get actionName() {
return ""; 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"> <table class="bulk-existing-action-list">
{ existingActions { existingActions
? existingActions ? existingActions
.map(action => { .map(action => action.render())
const renderedAction = action.render();
if (renderedAction) {
return <RawHtmlBlock
html={renderedAction[0].innerHTML}
style={{ display: "flex", alignItems: "center" }} />
} else {
return null;
}
})
.filter(renderedAction => renderedAction !== null) .filter(renderedAction => renderedAction !== null)
: <p>{t("bulk_actions.none_yet")}</p> : <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; id?: string;
name: string;
type?: HTMLInputTypeAttribute;
currentValue?: string; currentValue?: string;
className?: string;
autoComplete?: string;
onChange?(newValue: string): void; onChange?(newValue: string): void;
inputRef?: RefObject<HTMLInputElement>; 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 ( return (
<input <input
ref={inputRef} ref={inputRef}
@@ -21,6 +17,9 @@ export default function FormTextBox({ id, type, name, className, currentValue, o
name={name} name={name}
value={currentValue} value={currentValue}
autoComplete={autoComplete} autoComplete={autoComplete}
placeholder={placeholder}
title={title}
pattern={pattern}
onInput={e => onChange?.(e.currentTarget.value)} /> 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 { EventData, EventNames } from "../../components/app_context";
import { ParentComponent } from "./ReactBasicWidget"; import { ParentComponent } from "./ReactBasicWidget";
import SpacedUpdate from "../../services/spaced_update";
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) { export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentWidget = useContext(ParentComponent); const parentWidget = useContext(ParentComponent);
@@ -30,3 +31,11 @@ export default function useTriliumEvent<T extends EventNames>(eventName: T, hand
}; };
}, [parentWidget]); }, [parentWidget]);
} }
export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) {
const spacedUpdate = useMemo(() => {
return new SpacedUpdate(callback, interval);
}, [callback, interval]);
return spacedUpdate;
}