chore(client/ts): port search_definition

This commit is contained in:
Elian Doran 2025-02-24 13:45:36 +02:00
parent b91b243432
commit 8ab0084e10
No known key found for this signature in database
3 changed files with 95 additions and 52 deletions

View File

@ -288,6 +288,10 @@ type EventMappings = {
showHighlightsListWidget: { showHighlightsListWidget: {
noteId: string; noteId: string;
}; };
showSearchError: {
error: string;
};
searchRefreshed: { ntxId?: string | null };
hoistedNoteChanged: { hoistedNoteChanged: {
noteId: string; noteId: string;
ntxId: string | null; ntxId: string | null;

View File

@ -14,13 +14,15 @@ import OrderBy from "../search_options/order_by.js";
import SearchScript from "../search_options/search_script.js"; import SearchScript from "../search_options/search_script.js";
import Limit from "../search_options/limit.js"; import Limit from "../search_options/limit.js";
import Debug from "../search_options/debug.js"; import Debug from "../search_options/debug.js";
import appContext from "../../components/app_context.js"; import appContext, { type EventData } from "../../components/app_context.js";
import bulkActionService from "../../services/bulk_action.js"; import bulkActionService from "../../services/bulk_action.js";
import { Dropdown } from "bootstrap"; import { Dropdown } from "bootstrap";
import type FNote from "../../entities/fnote.js";
import type { AttributeType } from "../../entities/fattribute.js";
const TPL = ` const TPL = `
<div class="search-definition-widget"> <div class="search-definition-widget">
<style> <style>
.search-setting-table { .search-setting-table {
margin-top: 0; margin-top: 0;
margin-bottom: 7px; margin-bottom: 7px;
@ -28,28 +30,28 @@ const TPL = `
border-collapse: separate; border-collapse: separate;
border-spacing: 10px; border-spacing: 10px;
} }
.search-setting-table div { .search-setting-table div {
white-space: nowrap; white-space: nowrap;
} }
.search-setting-table .button-column { .search-setting-table .button-column {
/* minimal width so that table remains static sized and most space remains for middle column with settings */ /* minimal width so that table remains static sized and most space remains for middle column with settings */
width: 50px; width: 50px;
white-space: nowrap; white-space: nowrap;
text-align: right; text-align: right;
} }
.search-setting-table .title-column { .search-setting-table .title-column {
/* minimal width so that table remains static sized and most space remains for middle column with settings */ /* minimal width so that table remains static sized and most space remains for middle column with settings */
width: 50px; width: 50px;
white-space: nowrap; white-space: nowrap;
} }
.search-setting-table .button-column .dropdown-menu { .search-setting-table .button-column .dropdown-menu {
white-space: normal; white-space: normal;
} }
.attribute-list hr { .attribute-list hr {
height: 1px; height: 1px;
border-color: var(--main-border-color); border-color: var(--main-border-color);
@ -58,7 +60,7 @@ const TPL = `
margin-top: 5px; margin-top: 5px;
margin-bottom: 0; margin-bottom: 0;
} }
.search-definition-widget input:invalid { .search-definition-widget input:invalid {
border: 3px solid red; border: 3px solid red;
} }
@ -66,7 +68,7 @@ const TPL = `
.add-search-option button { .add-search-option button {
margin-top: 5px; /* to give some spacing when buttons overflow on the next line */ margin-top: 5px; /* to give some spacing when buttons overflow on the next line */
} }
.dropdown-header { .dropdown-header {
background-color: var(--accented-background-color); background-color: var(--accented-background-color);
} }
@ -78,47 +80,47 @@ const TPL = `
<td class="title-column">${t("search_definition.add_search_option")}</td> <td class="title-column">${t("search_definition.add_search_option")}</td>
<td colspan="2" class="add-search-option"> <td colspan="2" class="add-search-option">
<button type="button" class="btn btn-sm" data-search-option-add="searchString"> <button type="button" class="btn btn-sm" data-search-option-add="searchString">
<span class="bx bx-text"></span> <span class="bx bx-text"></span>
${t("search_definition.search_string")} ${t("search_definition.search_string")}
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="searchScript"> <button type="button" class="btn btn-sm" data-search-option-add="searchScript">
<span class="bx bx-code"></span> <span class="bx bx-code"></span>
${t("search_definition.search_script")} ${t("search_definition.search_script")}
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="ancestor"> <button type="button" class="btn btn-sm" data-search-option-add="ancestor">
<span class="bx bx-filter-alt"></span> <span class="bx bx-filter-alt"></span>
${t("search_definition.ancestor")} ${t("search_definition.ancestor")}
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="fastSearch" <button type="button" class="btn btn-sm" data-search-option-add="fastSearch"
title="${t("search_definition.fast_search_description")}"> title="${t("search_definition.fast_search_description")}">
<span class="bx bx-run"></span> <span class="bx bx-run"></span>
${t("search_definition.fast_search")} ${t("search_definition.fast_search")}
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="includeArchivedNotes" <button type="button" class="btn btn-sm" data-search-option-add="includeArchivedNotes"
title="${t("search_definition.include_archived_notes_description")}"> title="${t("search_definition.include_archived_notes_description")}">
<span class="bx bx-archive"></span> <span class="bx bx-archive"></span>
${t("search_definition.include_archived")} ${t("search_definition.include_archived")}
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="orderBy"> <button type="button" class="btn btn-sm" data-search-option-add="orderBy">
<span class="bx bx-arrow-from-top"></span> <span class="bx bx-arrow-from-top"></span>
${t("search_definition.order_by")} ${t("search_definition.order_by")}
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="limit" title="${t("search_definition.limit_description")}"> <button type="button" class="btn btn-sm" data-search-option-add="limit" title="${t("search_definition.limit_description")}">
<span class="bx bx-stop"></span> <span class="bx bx-stop"></span>
${t("search_definition.limit")} ${t("search_definition.limit")}
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="debug" title="${t("search_definition.debug_description")}"> <button type="button" class="btn btn-sm" data-search-option-add="debug" title="${t("search_definition.debug_description")}">
<span class="bx bx-bug"></span> <span class="bx bx-bug"></span>
${t("search_definition.debug")} ${t("search_definition.debug")}
</button> </button>
<div class="dropdown" style="display: inline-block;"> <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"> <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> <span class="bx bxs-zap"></span>
@ -138,12 +140,12 @@ const TPL = `
<span class="bx bx-search"></span> <span class="bx bx-search"></span>
${t("search_definition.search_button")} ${t("search_definition.search_button")}
</button> </button>
<button type="button" class="btn btn-sm search-and-execute-button"> <button type="button" class="btn btn-sm search-and-execute-button">
<span class="bx bxs-zap"></span> <span class="bx bxs-zap"></span>
${t("search_definition.search_execute")} ${t("search_definition.search_execute")}
</button> </button>
<button type="button" class="btn btn-sm save-to-note-button"> <button type="button" class="btn btn-sm save-to-note-button">
<span class="bx bx-save"></span> <span class="bx bx-save"></span>
${t("search_definition.save_to_note")} ${t("search_definition.save_to_note")}
@ -158,7 +160,21 @@ const TPL = `
const OPTION_CLASSES = [SearchString, SearchScript, Ancestor, FastSearch, IncludeArchivedNotes, OrderBy, Limit, Debug]; const OPTION_CLASSES = [SearchString, SearchScript, Ancestor, FastSearch, IncludeArchivedNotes, OrderBy, Limit, Debug];
// TODO: Deduplicate with server
interface SaveSearchNoteResponse {
notePath: string;
}
export default class SearchDefinitionWidget extends NoteContextAwareWidget { 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() { get name() {
return "searchDefinition"; return "searchDefinition";
} }
@ -194,7 +210,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
const searchOptionName = $(event.target).attr("data-search-option-add"); const searchOptionName = $(event.target).attr("data-search-option-add");
const clazz = OPTION_CLASSES.find((SearchOptionClass) => SearchOptionClass.optionName === searchOptionName); const clazz = OPTION_CLASSES.find((SearchOptionClass) => SearchOptionClass.optionName === searchOptionName);
if (clazz) { if (clazz && this.noteId) {
await clazz.create(this.noteId); await clazz.create(this.noteId);
} else { } else {
logError(t("search_definition.unknown_search_option", { searchOptionName })); logError(t("search_definition.unknown_search_option", { searchOptionName }));
@ -204,11 +220,13 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
}); });
this.$widget.on("click", "[data-action-add]", async (event) => { this.$widget.on("click", "[data-action-add]", async (event) => {
Dropdown.getOrCreateInstance(this.$widget.find(".action-add-toggle")); Dropdown.getOrCreateInstance(this.$widget.find(".action-add-toggle")[0]);
const actionName = $(event.target).attr("data-action-add"); const actionName = $(event.target).attr("data-action-add");
await bulkActionService.addAction(this.noteId, actionName); if (this.noteId && actionName) {
await bulkActionService.addAction(this.noteId, actionName);
}
this.refresh(); this.refresh();
}); });
@ -224,7 +242,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
this.$saveToNoteButton = this.$widget.find(".save-to-note-button"); this.$saveToNoteButton = this.$widget.find(".save-to-note-button");
this.$saveToNoteButton.on("click", async () => { this.$saveToNoteButton.on("click", async () => {
const { notePath } = await server.post("special-notes/save-search-note", { searchNoteId: this.noteId }); const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: this.noteId });
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
@ -236,24 +254,32 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
} }
async refreshResultsCommand() { async refreshResultsCommand() {
try { if (!this.noteId) {
const { error } = await froca.loadSearchNote(this.noteId); return;
}
if (error) { try {
this.handleEvent("showSearchError", { error }); const result = await froca.loadSearchNote(this.noteId);
if (result && result.error) {
this.handleEvent("showSearchError", { error: result.error });
} }
} catch (e) { } catch (e: any) {
toastService.showError(e.message); toastService.showError(e.message);
} }
this.triggerEvent("searchRefreshed", { ntxId: this.noteContext.ntxId }); this.triggerEvent("searchRefreshed", { ntxId: this.noteContext?.ntxId });
} }
async refreshSearchDefinitionCommand() { async refreshSearchDefinitionCommand() {
await this.refresh(); await this.refresh();
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote) {
if (!this.note) {
return;
}
this.$component.show(); this.$component.show();
this.$saveToNoteButton.toggle(note.isHiddenCompletely()); this.$saveToNoteButton.toggle(note.isHiddenCompletely());
@ -263,7 +289,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
for (const OptionClass of OPTION_CLASSES) { for (const OptionClass of OPTION_CLASSES) {
const { attributeType, optionName } = OptionClass; const { attributeType, optionName } = OptionClass;
const attr = this.note.getAttribute(attributeType, optionName); const attr = this.note.getAttribute(attributeType as AttributeType, optionName);
this.$widget.find(`[data-search-option-add='${optionName}'`).toggle(!attr); this.$widget.find(`[data-search-option-add='${optionName}'`).toggle(!attr);
@ -271,14 +297,19 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
const searchOption = new OptionClass(attr, this.note).setParent(this); const searchOption = new OptionClass(attr, this.note).setParent(this);
this.child(searchOption); this.child(searchOption);
this.$searchOptions.append(searchOption.render()); const renderedEl = searchOption.render();
if (renderedEl) {
this.$searchOptions.append(renderedEl);
}
} }
} }
const actions = bulkActionService.parseActions(this.note); const actions = bulkActionService.parseActions(this.note);
const renderedEls = actions
.map((action) => action.render())
.filter((e) => e) as JQuery<HTMLElement>[];
this.$actionOptions.empty().append(...actions.map((action) => action.render())); this.$actionOptions.empty().append(...renderedEls);
this.$searchAndExecuteButton.css("visibility", actions.length > 0 ? "visible" : "_hidden"); this.$searchAndExecuteButton.css("visibility", actions.length > 0 ? "visible" : "_hidden");
} }
@ -294,7 +325,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
toastService.showMessage(t("search_definition.actions_executed"), 3000); toastService.showMessage(t("search_definition.actions_executed"), 3000);
} }
entitiesReloadedEvent({ loadResults }) { entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// only refreshing deleted attrs, otherwise components update themselves // only refreshing deleted attrs, otherwise components update themselves
if (loadResults.getAttributeRows().find((attrRow) => attrRow.type === "label" && attrRow.name === "action" && attrRow.isDeleted)) { if (loadResults.getAttributeRows().find((attrRow) => attrRow.type === "label" && attrRow.name === "action" && attrRow.isDeleted)) {
this.refresh(); this.refresh();

View File

@ -2,24 +2,32 @@ import server from "../../services/server.js";
import ws from "../../services/ws.js"; import ws from "../../services/ws.js";
import Component from "../../components/component.js"; import Component from "../../components/component.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js"; // 新增的导入 import { t } from "../../services/i18n.js";
import type FAttribute from "../../entities/fattribute.js";
import type FNote from "../../entities/fnote.js";
import type { AttributeType } from "../../entities/fattribute.js";
export default class AbstractSearchOption extends Component { export default abstract class AbstractSearchOption extends Component {
constructor(attribute, note) {
private attribute: FAttribute;
private note: FNote;
constructor(attribute: FAttribute, note: FNote) {
super(); super();
this.attribute = attribute; this.attribute = attribute;
this.note = note; this.note = note;
} }
static async setAttribute(noteId, type, name, value = "") { static async setAttribute(noteId: string, type: AttributeType, name: string, value: string = "") {
await server.put(`notes/${noteId}/set-attribute`, { type, name, value }); await server.put(`notes/${noteId}/set-attribute`, { type, name, value });
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
} }
async setAttribute(type, name, value = "") { async setAttribute(type: AttributeType, name: string, value: string = "") {
await this.constructor.setAttribute(this.note.noteId, type, name, value); // TODO: Find a better pattern.
await (this.constructor as any).setAttribute(this.note.noteId, type, name, value);
} }
render() { render() {
@ -29,29 +37,29 @@ export default class AbstractSearchOption extends Component {
$rendered $rendered
.find(".search-option-del") .find(".search-option-del")
.on("click", () => this.deleteOption()) .on("click", () => this.deleteOption())
.attr("title", t("abstract_search_option.remove_this_search_option")); // 使用 t 函数处理 i18n 字符串 .attr("title", t("abstract_search_option.remove_this_search_option"));
utils.initHelpDropdown($rendered); utils.initHelpDropdown($rendered);
return $rendered; return $rendered;
} catch (e) { } catch (e: any) {
logError(t("abstract_search_option.failed_rendering", { dto: JSON.stringify(this.attribute.dto), error: e.message, stack: e.stack })); // 使用 t 函数处理 i18n 字符串 logError(t("abstract_search_option.failed_rendering", { dto: JSON.stringify(this.attribute.dto), error: e.message, stack: e.stack }));
return null; return null;
} }
} }
// to be overridden abstract doRender(): JQuery<HTMLElement>;
doRender() {}
async deleteOption() { async deleteOption() {
await this.deleteAttribute(this.constructor.attributeType, this.constructor.optionName); // TODO: Find a better pattern.
await this.deleteAttribute((this.constructor as any).attributeType, (this.constructor as any).optionName);
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await this.triggerCommand("refreshSearchDefinition"); await this.triggerCommand("refreshSearchDefinition");
} }
async deleteAttribute(type, name) { async deleteAttribute(type: AttributeType, name: string) {
for (const attr of this.note.getOwnedAttributes()) { for (const attr of this.note.getOwnedAttributes()) {
if (attr.type === type && attr.name === name) { if (attr.type === type && attr.name === name) {
await server.remove(`notes/${this.note.noteId}/attributes/${attr.attributeId}`); await server.remove(`notes/${this.note.noteId}/attributes/${attr.attributeId}`);