mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 07:38:53 +02:00
chore(react/ribbon): port bulk actions for search
This commit is contained in:
parent
3218ab971b
commit
99a911a220
@ -18,7 +18,7 @@ import type FNote from "../entities/fnote.js";
|
|||||||
import toast from "./toast.js";
|
import toast from "./toast.js";
|
||||||
import { BulkAction } from "@triliumnext/commons";
|
import { BulkAction } from "@triliumnext/commons";
|
||||||
|
|
||||||
const ACTION_GROUPS = [
|
export const ACTION_GROUPS = [
|
||||||
{
|
{
|
||||||
title: t("bulk_actions.labels"),
|
title: t("bulk_actions.labels"),
|
||||||
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||||
|
@ -13,11 +13,12 @@ export interface DropdownProps {
|
|||||||
dropdownContainerStyle?: CSSProperties;
|
dropdownContainerStyle?: CSSProperties;
|
||||||
dropdownContainerClassName?: string;
|
dropdownContainerClassName?: string;
|
||||||
hideToggleArrow?: boolean;
|
hideToggleArrow?: boolean;
|
||||||
|
noSelectButtonStyle?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
text?: ComponentChildren;
|
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 dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre
|
|||||||
return (
|
return (
|
||||||
<div ref={dropdownRef} class={`dropdown ${className ?? ""}`} style={{ display: "flex" }}>
|
<div ref={dropdownRef} class={`dropdown ${className ?? ""}`} style={{ display: "flex" }}>
|
||||||
<button
|
<button
|
||||||
className={`btn select-button ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
|
className={`btn ${!noSelectButtonStyle ? "select-button" : ""} ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { VNode } from "preact";
|
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { TabContext } from "./ribbon-interface";
|
import { TabContext } from "./ribbon-interface";
|
||||||
@ -15,6 +14,11 @@ import server from "../../services/server";
|
|||||||
import ws from "../../services/ws";
|
import ws from "../../services/ws";
|
||||||
import tree from "../../services/tree";
|
import tree from "../../services/tree";
|
||||||
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
|
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) {
|
export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
@ -77,12 +81,15 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
|
|||||||
<td colSpan={2} className="add-search-option">
|
<td colSpan={2} className="add-search-option">
|
||||||
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
||||||
<Button
|
<Button
|
||||||
|
size="small"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
text={label}
|
text={label}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<AddBulkActionButton note={note} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tbody className="search-options">
|
<tbody className="search-options">
|
||||||
@ -98,9 +105,7 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
|
|||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tbody className="action-options">
|
<BulkActionsList note={note} />
|
||||||
|
|
||||||
</tbody>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={3}>
|
<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>
|
||||||
|
)
|
||||||
|
}
|
@ -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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user