mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	chore(react/ribbon): port bulk actions for search
This commit is contained in:
		| @@ -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"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| } |  | ||||||
		Reference in New Issue
	
	Block a user