Files
Trilium/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx

153 lines
5.7 KiB
TypeScript
Raw Normal View History

import { VNode } from "preact";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { TabContext } from "./ribbon-interface";
import { SaveSearchNoteResponse } from "@triliumnext/commons";
import attributes from "../../services/attributes";
import FNote from "../../entities/fnote";
import toast from "../../services/toast";
import froca from "../../services/froca";
import { useContext, useEffect, useState } from "preact/hooks";
import { ParentComponent } from "../react/react_utils";
import { useTriliumEventBeta } from "../react/hooks";
import appContext from "../../components/app_context";
import server from "../../services/server";
import ws from "../../services/ws";
import tree from "../../services/tree";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
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[] }>();
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 });
}
async function refreshResults() {
const noteId = note?.noteId;
if (!noteId) {
return;
}
try {
const result = await froca.loadSearchNote(noteId);
if (result?.error) {
setError({ message: result?.error})
} else {
setError(undefined);
}
} catch (e: any) {
toast.showError(e.message);
}
parentComponent?.triggerEvent("searchRefreshed", { ntxId });
}
2025-08-24 16:17:10 +03:00
// Refresh the list of available and active options.
useEffect(refreshOptions, [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) {
refreshOptions();
}
});
return (
<div className="search-definition-widget">
<div className="search-settings">
{note &&
<table className="search-setting-table">
<tr>
<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 }) => (
<Button
icon={icon}
text={label}
title={tooltip}
2025-08-24 18:29:47 +03:00
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
</td>
</tr>
<tbody className="search-options">
2025-08-24 18:34:29 +03:00
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
2025-08-24 16:17:10 +03:00
return component?.({
attributeName,
attributeType,
note,
refreshResults,
error,
2025-08-24 18:34:29 +03:00
additionalAttributesToDelete,
defaultValue
2025-08-24 16:17:10 +03:00
});
})}
</tbody>
<tbody className="action-options">
</tbody>
<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}
/>
<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);
}}
/>
{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) }));
}}
/>}
</div>
</td>
</tr>
</tbody>
</table>
}
</div>
</div>
)
}