2025-08-24 11:38:50 +03:00
|
|
|
import { t } from "../../services/i18n";
|
|
|
|
|
import Button from "../react/Button";
|
|
|
|
|
import { TabContext } from "./ribbon-interface";
|
2025-08-24 18:40:05 +03:00
|
|
|
import { SaveSearchNoteResponse } from "@triliumnext/commons";
|
|
|
|
|
import attributes from "../../services/attributes";
|
2025-08-24 15:29:07 +03:00
|
|
|
import FNote from "../../entities/fnote";
|
2025-08-24 15:48:53 +03:00
|
|
|
import toast from "../../services/toast";
|
|
|
|
|
import froca from "../../services/froca";
|
2025-08-24 18:40:05 +03:00
|
|
|
import { useContext, useEffect, useState } from "preact/hooks";
|
2025-08-24 15:48:53 +03:00
|
|
|
import { ParentComponent } from "../react/react_utils";
|
2025-08-24 21:18:48 +03:00
|
|
|
import { useTriliumEvent } from "../react/hooks";
|
2025-08-24 15:48:53 +03:00
|
|
|
import appContext from "../../components/app_context";
|
|
|
|
|
import server from "../../services/server";
|
2025-08-24 16:56:22 +03:00
|
|
|
import ws from "../../services/ws";
|
|
|
|
|
import tree from "../../services/tree";
|
2025-08-24 18:40:05 +03:00
|
|
|
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
|
2025-08-24 20:12:22 +03:00
|
|
|
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";
|
2025-08-25 17:17:56 +03:00
|
|
|
import { getErrorMessage } from "../../services/utils";
|
2025-08-24 11:38:50 +03:00
|
|
|
|
2025-08-24 15:48:53 +03:00
|
|
|
export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
|
|
|
|
|
const parentComponent = useContext(ParentComponent);
|
2025-08-24 16:17:10 +03:00
|
|
|
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
|
2025-08-24 16:41:44 +03:00
|
|
|
const [ error, setError ] = useState<{ message: string }>();
|
2025-08-24 16:17:10 +03:00
|
|
|
|
|
|
|
|
function refreshOptions() {
|
|
|
|
|
if (!note) return;
|
|
|
|
|
|
|
|
|
|
const availableOptions: SearchOption[] = [];
|
|
|
|
|
const activeOptions: SearchOption[] = [];
|
|
|
|
|
|
|
|
|
|
for (const searchOption of SEARCH_OPTIONS) {
|
|
|
|
|
const attr = note.getAttribute(searchOption.attributeType, searchOption.attributeName);
|
|
|
|
|
if (attr && searchOption.component) {
|
|
|
|
|
activeOptions.push(searchOption);
|
|
|
|
|
} else {
|
|
|
|
|
availableOptions.push(searchOption);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSearchOptions({ availableOptions, activeOptions });
|
|
|
|
|
}
|
2025-08-24 15:48:53 +03:00
|
|
|
|
|
|
|
|
async function refreshResults() {
|
|
|
|
|
const noteId = note?.noteId;
|
|
|
|
|
if (!noteId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await froca.loadSearchNote(noteId);
|
2025-08-24 16:41:44 +03:00
|
|
|
if (result?.error) {
|
|
|
|
|
setError({ message: result?.error})
|
|
|
|
|
} else {
|
|
|
|
|
setError(undefined);
|
2025-08-24 15:48:53 +03:00
|
|
|
}
|
2025-08-25 17:17:56 +03:00
|
|
|
} catch (e: unknown) {
|
|
|
|
|
toast.showError(getErrorMessage(e));
|
2025-08-24 15:48:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parentComponent?.triggerEvent("searchRefreshed", { ntxId });
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 16:17:10 +03:00
|
|
|
// Refresh the list of available and active options.
|
|
|
|
|
useEffect(refreshOptions, [ note ]);
|
2025-08-24 21:18:48 +03:00
|
|
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
2025-08-24 16:17:10 +03:00
|
|
|
if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) {
|
|
|
|
|
refreshOptions();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-24 11:38:50 +03:00
|
|
|
return (
|
|
|
|
|
<div className="search-definition-widget">
|
|
|
|
|
<div className="search-settings">
|
2025-08-24 15:59:22 +03:00
|
|
|
{note &&
|
|
|
|
|
<table className="search-setting-table">
|
2025-08-24 15:48:53 +03:00
|
|
|
<tr>
|
2025-08-24 15:59:22 +03:00
|
|
|
<td className="title-column">{t("search_definition.add_search_option")}</td>
|
|
|
|
|
<td colSpan={2} className="add-search-option">
|
2025-08-24 18:29:47 +03:00
|
|
|
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
2025-08-24 15:48:53 +03:00
|
|
|
<Button
|
2025-08-24 20:12:22 +03:00
|
|
|
size="small"
|
2025-08-24 15:59:22 +03:00
|
|
|
icon={icon}
|
|
|
|
|
text={label}
|
|
|
|
|
title={tooltip}
|
2025-08-24 18:29:47 +03:00
|
|
|
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
2025-08-24 15:48:53 +03:00
|
|
|
/>
|
2025-08-24 15:59:22 +03:00
|
|
|
))}
|
2025-08-24 20:12:22 +03:00
|
|
|
|
|
|
|
|
<AddBulkActionButton note={note} />
|
2025-08-24 15:48:53 +03:00
|
|
|
</td>
|
|
|
|
|
</tr>
|
2025-08-24 15:59:22 +03:00
|
|
|
<tbody className="search-options">
|
2025-08-24 18:34:29 +03:00
|
|
|
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
|
2025-08-27 17:46:20 +03:00
|
|
|
const Component = component;
|
|
|
|
|
return <Component
|
|
|
|
|
attributeName={attributeName}
|
|
|
|
|
attributeType={attributeType}
|
|
|
|
|
note={note}
|
|
|
|
|
refreshResults={refreshResults}
|
|
|
|
|
error={error}
|
|
|
|
|
additionalAttributesToDelete={additionalAttributesToDelete}
|
|
|
|
|
defaultValue={defaultValue}
|
|
|
|
|
/>;
|
2025-08-24 15:59:22 +03:00
|
|
|
})}
|
|
|
|
|
</tbody>
|
2025-08-24 20:12:22 +03:00
|
|
|
<BulkActionsList note={note} />
|
2025-08-24 15:59:22 +03:00
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td colSpan={3}>
|
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
|
|
|
|
|
<Button
|
|
|
|
|
icon="bx bx-search"
|
|
|
|
|
text={t("search_definition.search_button")}
|
|
|
|
|
keyboardShortcut="Enter"
|
|
|
|
|
onClick={refreshResults}
|
|
|
|
|
/>
|
2025-08-24 16:02:15 +03:00
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
icon="bx bxs-zap"
|
|
|
|
|
text={t("search_definition.search_execute")}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
await server.post(`search-and-execute-note/${note.noteId}`);
|
|
|
|
|
refreshResults();
|
|
|
|
|
toast.showMessage(t("search_definition.actions_executed"), 3000);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-08-24 16:56:22 +03:00
|
|
|
|
|
|
|
|
{note.isHiddenCompletely() && <Button
|
|
|
|
|
icon="bx bx-save"
|
|
|
|
|
text={t("search_definition.save_to_note")}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
|
|
|
|
|
if (!notePath) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ws.waitForMaxKnownEntityChangeId();
|
|
|
|
|
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
|
|
|
|
|
|
|
|
|
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
|
|
|
|
|
// See https://www.i18next.com/translation-function/interpolation#unescape
|
|
|
|
|
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
|
|
|
|
|
}}
|
|
|
|
|
/>}
|
2025-08-24 15:59:22 +03:00
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
}
|
2025-08-24 11:38:50 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2025-08-24 15:29:07 +03:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 20:12:22 +03:00
|
|
|
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 ]);
|
2025-08-24 21:18:48 +03:00
|
|
|
useTriliumEvent("entitiesReloaded", ({loadResults}) => {
|
2025-08-24 20:12:22 +03:00
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
}
|