chore(react/ribbon): port bulk actions for search

This commit is contained in:
Elian Doran 2025-08-24 20:12:22 +03:00
parent 3218ab971b
commit 99a911a220
No known key found for this signature in database
4 changed files with 58 additions and 111 deletions

View File

@ -18,7 +18,7 @@ import type FNote from "../entities/fnote.js";
import toast from "./toast.js";
import { BulkAction } from "@triliumnext/commons";
const ACTION_GROUPS = [
export const ACTION_GROUPS = [
{
title: t("bulk_actions.labels"),
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]

View File

@ -13,11 +13,12 @@ export interface DropdownProps {
dropdownContainerStyle?: CSSProperties;
dropdownContainerClassName?: string;
hideToggleArrow?: boolean;
noSelectButtonStyle?: boolean;
disabled?: boolean;
text?: ComponentChildren;
}
export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, disabled }: DropdownProps) {
export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, disabled, noSelectButtonStyle }: DropdownProps) {
const dropdownRef = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLButtonElement | null>(null);
@ -57,7 +58,7 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre
return (
<div ref={dropdownRef} class={`dropdown ${className ?? ""}`} style={{ display: "flex" }}>
<button
className={`btn select-button ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
className={`btn ${!noSelectButtonStyle ? "select-button" : ""} ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
ref={triggerRef}
type="button"
data-bs-toggle="dropdown"

View File

@ -1,4 +1,3 @@
import { VNode } from "preact";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { TabContext } from "./ribbon-interface";
@ -15,6 +14,11 @@ import server from "../../services/server";
import ws from "../../services/ws";
import tree from "../../services/tree";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
import Dropdown from "../react/Dropdown";
import Icon from "../react/Icon";
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
import { FormListHeader, FormListItem } from "../react/FormList";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
const parentComponent = useContext(ParentComponent);
@ -77,12 +81,15 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
<td colSpan={2} className="add-search-option">
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<Button
size="small"
icon={icon}
text={label}
title={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
<AddBulkActionButton note={note} />
</td>
</tr>
<tbody className="search-options">
@ -98,9 +105,7 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
});
})}
</tbody>
<tbody className="action-options">
</tbody>
<BulkActionsList note={note} />
<tbody>
<tr>
<td colSpan={3}>
@ -150,3 +155,48 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
)
}
function BulkActionsList({ note }: { note: FNote }) {
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
function refreshBulkActions() {
if (note) {
setBulkActions(bulk_action.parseActions(note));
}
}
// React to changes.
useEffect(refreshBulkActions, [ note ]);
useTriliumEventBeta("entitiesReloaded", ({loadResults}) => {
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "action" && attributes.isAffecting(attr, note))) {
refreshBulkActions();
}
});
return (
<tbody className="action-options">
{bulkActions?.map(bulkAction => (
bulkAction.doRender()
))}
</tbody>
)
}
function AddBulkActionButton({ note }: { note: FNote }) {
return (
<Dropdown
buttonClassName="action-add-toggle btn-sm"
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
noSelectButtonStyle
>
{ACTION_GROUPS.map(({ actions, title }) => (
<>
<FormListHeader text={title} />
{actions.map(({ actionName, actionTitle }) => (
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
))}
</>
))}
</Dropdown>
)
}

View File

@ -1,104 +0,0 @@
import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import froca from "../../services/froca.js";
import ws from "../../services/ws.js";
import toastService from "../../services/toast.js";
import treeService from "../../services/tree.js";
import SearchString from "../search_options/search_string.js";
import FastSearch from "../search_options/fast_search.js";
import Ancestor from "../search_options/ancestor.js";
import IncludeArchivedNotes from "../search_options/include_archived_notes.js";
import OrderBy from "../search_options/order_by.js";
import SearchScript from "../search_options/search_script.js";
import Limit from "../search_options/limit.js";
import Debug from "../search_options/debug.js";
import appContext, { type EventData } from "../../components/app_context.js";
import bulkActionService from "../../services/bulk_action.js";
import { Dropdown } from "bootstrap";
import type FNote from "../../entities/fnote.js";
import type { AttributeType } from "../../entities/fattribute.js";
import { renderReactWidget } from "../react/react_utils.jsx";
const TPL = /*html*/`
<div class="">
<div class="">
<tr>
<td>
<div class="dropdown" style="display: inline-block;">
<button class="btn btn-sm dropdown-toggle action-add-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="bx bxs-zap"></span>
${t("search_definition.action")}
</button>
<div class="dropdown-menu action-list"></div>
</div>
</td>
</tr>
<tbody class="search-options"></tbody>
<tbody class="action-options"></tbody>
</table>
</div>
</div>`;
const OPTION_CLASSES = [SearchString, SearchScript, Ancestor, FastSearch, IncludeArchivedNotes, OrderBy, Limit, Debug];
export default class SearchDefinitionWidget extends NoteContextAwareWidget {
private $component!: JQuery<HTMLElement>;
private $actionList!: JQuery<HTMLElement>;
private $searchOptions!: JQuery<HTMLElement>;
private $searchButton!: JQuery<HTMLElement>;
private $searchAndExecuteButton!: JQuery<HTMLElement>;
private $saveToNoteButton!: JQuery<HTMLElement>;
private $actionOptions!: JQuery<HTMLElement>;
get name() {
return "searchDefinition";
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$component = this.$widget.find(".search-definition-widget");
this.$actionList = this.$widget.find(".action-list");
for (const actionGroup of bulkActionService.ACTION_GROUPS) {
this.$actionList.append($('<h6 class="dropdown-header">').append(actionGroup.title));
for (const action of actionGroup.actions) {
this.$actionList.append($('<a class="dropdown-item" href="#">').attr("data-action-add", action.actionName).text(action.actionTitle));
}
}
this.$widget.on("click", "[data-action-add]", async (event) => {
Dropdown.getOrCreateInstance(this.$widget.find(".action-add-toggle")[0]);
const actionName = $(event.target).attr("data-action-add");
if (this.noteId && actionName) {
await bulkActionService.addAction(this.noteId, actionName);
}
this.refresh();
});
this.$searchOptions = this.$widget.find(".search-options");
this.$actionOptions = this.$widget.find(".action-options");
}
async refreshWithNote(note: FNote) {
if (!this.note) {
return;
}
const actions = bulkActionService.parseActions(this.note);
const renderedEls = actions
.map((action) => renderReactWidget(this, action.doRender()))
.filter((e) => e) as JQuery<HTMLElement>[];
this.$actionOptions.empty().append(...renderedEls);
this.$searchAndExecuteButton.css("visibility", actions.length > 0 ? "visible" : "_hidden");
}
}