mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 19:05:59 +01:00
feat(react/bulk_actions): port add_label
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
56
apps/client/src/widgets/bulk_actions/label/add_label.tsx
Normal file
56
apps/client/src/widgets/bulk_actions/label/add_label.tsx
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)} />
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user