2025-08-24 18:40:05 +03:00
|
|
|
import { VNode } from "preact";
|
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 18:40:05 +03:00
|
|
|
import { useTriliumEventBeta } 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 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
|
|
|
}
|
|
|
|
|
} 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();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
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 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 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-24 16:17:10 +03:00
|
|
|
return component?.({
|
|
|
|
|
attributeName,
|
|
|
|
|
attributeType,
|
|
|
|
|
note,
|
2025-08-24 16:41:44 +03:00
|
|
|
refreshResults,
|
2025-08-24 18:02:18 +03:00
|
|
|
error,
|
2025-08-24 18:34:29 +03:00
|
|
|
additionalAttributesToDelete,
|
|
|
|
|
defaultValue
|
2025-08-24 16:17:10 +03:00
|
|
|
});
|
2025-08-24 15:59:22 +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}
|
|
|
|
|
/>
|
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
|
|
|
}
|
|
|
|
|
|