From 274e3c1f7f8e1386afa9fdc34dee94578058ec78 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 24 Aug 2025 18:40:05 +0300 Subject: [PATCH] refactor(react/ribbon): split into two files --- .../ribbon/SearchDefinitionOptions.tsx | 364 +++++++++++++++++ .../widgets/ribbon/SearchDefinitionTab.tsx | 366 +----------------- 2 files changed, 370 insertions(+), 360 deletions(-) create mode 100644 apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx new file mode 100644 index 000000000..b7ded28a1 --- /dev/null +++ b/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx @@ -0,0 +1,364 @@ +import Dropdown from "../react/Dropdown"; +import ActionButton from "../react/ActionButton"; +import FormTextArea from "../react/FormTextArea"; +import NoteAutocomplete from "../react/NoteAutocomplete"; +import FormSelect from "../react/FormSelect"; +import Icon from "../react/Icon"; +import FormTextBox from "../react/FormTextBox"; +import { ComponentChildren, VNode } from "preact"; +import FNote from "../../entities/fnote"; +import { removeOwnedAttributesByNameOrType } from "../../services/attributes"; +import { AttributeType } from "@triliumnext/commons"; +import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks"; +import { t } from "../../services/i18n"; +import { useEffect, useMemo, useRef } from "preact/hooks"; +import appContext from "../../components/app_context"; +import server from "../../services/server"; + +export interface SearchOption { + attributeName: string; + attributeType: "label" | "relation"; + icon: string; + label: string; + tooltip?: string; + component?: (props: SearchOptionProps) => VNode; + defaultValue?: string; + additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; +} + +interface SearchOptionProps { + note: FNote; + refreshResults: () => void; + attributeName: string; + attributeType: "label" | "relation"; + additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; + defaultValue?: string; + error?: { message: string }; +} + +export const SEARCH_OPTIONS: SearchOption[] = [ + { + attributeName: "searchString", + attributeType: "label", + icon: "bx bx-text", + label: t("search_definition.search_string"), + component: SearchStringOption + }, + { + attributeName: "searchScript", + attributeType: "relation", + defaultValue: "root", + icon: "bx bx-code", + label: t("search_definition.search_script"), + component: SearchScriptOption + }, + { + attributeName: "ancestor", + attributeType: "relation", + defaultValue: "root", + icon: "bx bx-filter-alt", + label: t("search_definition.ancestor"), + component: AncestorOption, + additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ] + }, + { + attributeName: "fastSearch", + attributeType: "label", + icon: "bx bx-run", + label: t("search_definition.fast_search"), + tooltip: t("search_definition.fast_search_description"), + component: FastSearchOption + }, + { + attributeName: "includeArchivedNotes", + attributeType: "label", + icon: "bx bx-archive", + label: t("search_definition.include_archived"), + tooltip: t("search_definition.include_archived_notes_description"), + component: IncludeArchivedNotesOption + }, + { + attributeName: "orderBy", + attributeType: "label", + defaultValue: "relevancy", + icon: "bx bx-arrow-from-top", + label: t("search_definition.order_by"), + component: OrderByOption, + additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ] + }, + { + attributeName: "limit", + attributeType: "label", + defaultValue: "10", + icon: "bx bx-stop", + label: t("search_definition.limit"), + tooltip: t("search_definition.limit_description"), + component: LimitOption + }, + { + attributeName: "debug", + attributeType: "label", + icon: "bx bx-bug", + label: t("search_definition.debug"), + tooltip: t("search_definition.debug_description"), + component: DebugOption + } +]; + +function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: { + note: FNote; + title: string, + titleIcon: string, + children?: ComponentChildren, + help: ComponentChildren, + attributeName: string, + attributeType: AttributeType, + additionalAttributesToDelete: { type: "label" | "relation", name: string }[] +}) { + return ( + + + {titleIcon && <>{" "}} + {title} + + {children} + + {help && {help}} + { + removeOwnedAttributesByNameOrType(note, attributeType, attributeName); + if (additionalAttributesToDelete) { + for (const { type, name } of additionalAttributesToDelete) { + removeOwnedAttributesByNameOrType(note, type, name); + } + } + }} + /> + + + ) +} + +function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) { + const [ searchString, setSearchString ] = useNoteLabel(note, "searchString"); + const inputRef = useRef(null); + const currentValue = useRef(searchString ?? ""); + const spacedUpdate = useSpacedUpdate(async () => { + const searchString = currentValue.current; + appContext.lastSearchString = searchString; + setSearchString(searchString); + + if (note.title.startsWith(t("search_string.search_prefix"))) { + await server.put(`notes/${note.noteId}/title`, { + title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}` + }); + } + }, 1000); + + // React to errors + const { showTooltip, hideTooltip } = useTooltip(inputRef, { + trigger: "manual", + title: `${t("search_string.error", { error: error?.message })}`, + html: true, + placement: "bottom" + }); + + // Auto-focus. + useEffect(() => inputRef.current?.focus(), []); + + useEffect(() => { + if (error) { + showTooltip(); + setTimeout(() => hideTooltip(), 4000); + } else { + hideTooltip(); + } + }, [ error ]); + + return + {t("search_string.search_syntax")} - {t("search_string.also_see")} {t("search_string.complete_help")} +
    +
  • {t("search_string.full_text_search")}
  • +
  • #abc - {t("search_string.label_abc")}
  • +
  • #year = 2019 - {t("search_string.label_year")}
  • +
  • #rock #pop - {t("search_string.label_rock_pop")}
  • +
  • #rock or #pop - {t("search_string.label_rock_or_pop")}
  • +
  • #year <= 2000 - {t("search_string.label_year_comparison")}
  • +
  • note.dateCreated >= MONTH-1 - {t("search_string.label_date_created")}
  • +
+ } + note={note} {...restProps} + > + { + currentValue.current = text; + spacedUpdate.scheduleUpdate(); + }} + onKeyDown={async (e) => { + if (e.key === "Enter") { + e.preventDefault(); + + // this also in effect disallows new lines in query string. + // on one hand, this makes sense since search string is a label + // on the other hand, it could be nice for structuring long search string. It's probably a niche case though. + await spacedUpdate.updateNowIfNecessary(); + refreshResults(); + } + }} + /> +
+} + +function SearchScriptOption({ note, ...restProps }: SearchOptionProps) { + const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript"); + + return +

{t("search_script.description1")}

+

{t("search_script.description2")}

+

{t("search_script.example_title")}

+
{t("search_script.example_code")}
+ {t("search_script.note")} + } + note={note} {...restProps} + > + setSearchScript(noteId ?? "root")} + placeholder={t("search_script.placeholder")} + /> +
+} + +function AncestorOption({ note, ...restProps}: SearchOptionProps) { + const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor"); + const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth"); + + const options = useMemo(() => { + const options: { value: string | undefined; label: string }[] = [ + { value: "", label: t("ancestor.depth_doesnt_matter") }, + { value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` } + ]; + + for (let i=2; i<=9; i++) options.push({ value: "eq" + i, label: t("ancestor.depth_eq", { count: i }) }); + for (let i=0; i<=9; i++) options.push({ value: "gt" + i, label: t("ancestor.depth_gt", { count: i }) }); + for (let i=2; i<=9; i++) options.push({ value: "lt" + i, label: t("ancestor.depth_lt", { count: i }) }); + + return options; + }, []); + + return +
+ setAncestor(noteId ?? "root")} + placeholder={t("ancestor.placeholder")} + /> + +
{t("ancestor.depth_label")}:
+ setDepth(value ? value : null)} + style={{ flexShrink: 3 }} + /> +
+
; +} + +function FastSearchOption({ ...restProps }: SearchOptionProps) { + return +} + +function DebugOption({ ...restProps }: SearchOptionProps) { + return +

{t("debug.debug_info")}

+ {t("debug.access_info")} + } + {...restProps} + /> +} + +function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) { + return +} + +function OrderByOption({ note, ...restProps }: SearchOptionProps) { + const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy"); + const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection"); + + return + + {" "} + + +} + +function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) { + const [ limit, setLimit ] = useNoteLabel(note, "limit"); + + return + + +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx index 3490e296d..eead1e911 100644 --- a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx +++ b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx @@ -1,117 +1,20 @@ -import { ComponentChildren, VNode } from "preact"; +import { VNode } from "preact"; import { t } from "../../services/i18n"; import Button from "../react/Button"; import { TabContext } from "./ribbon-interface"; -import Dropdown from "../react/Dropdown"; -import ActionButton from "../react/ActionButton"; -import FormTextArea from "../react/FormTextArea"; -import { AttributeType, SaveSearchNoteResponse } from "@triliumnext/commons"; -import attributes, { removeOwnedAttributesByNameOrType } from "../../services/attributes"; +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, useMemo, useRef, useState } from "preact/hooks"; +import { useContext, useEffect, useState } from "preact/hooks"; import { ParentComponent } from "../react/react_utils"; -import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip, useTriliumEventBeta } from "../react/hooks"; +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 NoteAutocomplete from "../react/NoteAutocomplete"; -import FormSelect from "../react/FormSelect"; -import Icon from "../react/Icon"; -import FormTextBox from "../react/FormTextBox"; - -interface SearchOption { - attributeName: string; - attributeType: "label" | "relation"; - icon: string; - label: string; - tooltip?: string; - // TODO: Make mandatory once all components are ported. - component?: (props: SearchOptionProps) => VNode; - defaultValue?: string; - additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; -} - -interface SearchOptionProps { - note: FNote; - refreshResults: () => void; - attributeName: string; - attributeType: "label" | "relation"; - additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; - defaultValue?: string; - error?: { message: string }; -} - -const SEARCH_OPTIONS: SearchOption[] = [ - { - attributeName: "searchString", - attributeType: "label", - icon: "bx bx-text", - label: t("search_definition.search_string"), - component: SearchStringOption - }, - { - attributeName: "searchScript", - attributeType: "relation", - defaultValue: "root", - icon: "bx bx-code", - label: t("search_definition.search_script"), - component: SearchScriptOption - }, - { - attributeName: "ancestor", - attributeType: "relation", - defaultValue: "root", - icon: "bx bx-filter-alt", - label: t("search_definition.ancestor"), - component: AncestorOption, - additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ] - }, - { - attributeName: "fastSearch", - attributeType: "label", - icon: "bx bx-run", - label: t("search_definition.fast_search"), - tooltip: t("search_definition.fast_search_description"), - component: FastSearchOption - }, - { - attributeName: "includeArchivedNotes", - attributeType: "label", - icon: "bx bx-archive", - label: t("search_definition.include_archived"), - tooltip: t("search_definition.include_archived_notes_description"), - component: IncludeArchivedNotesOption - }, - { - attributeName: "orderBy", - attributeType: "label", - defaultValue: "relevancy", - icon: "bx bx-arrow-from-top", - label: t("search_definition.order_by"), - component: OrderByOption, - additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ] - }, - { - attributeName: "limit", - attributeType: "label", - defaultValue: "10", - icon: "bx bx-stop", - label: t("search_definition.limit"), - tooltip: t("search_definition.limit_description"), - component: LimitOption - }, - { - attributeName: "debug", - attributeType: "label", - icon: "bx bx-bug", - label: t("search_definition.debug"), - tooltip: t("search_definition.debug_description"), - component: DebugOption - } -]; +import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions"; export default function SearchDefinitionTab({ note, ntxId }: TabContext) { const parentComponent = useContext(ParentComponent); @@ -247,260 +150,3 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) { ) } -function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: { - note: FNote; - title: string, - titleIcon: string, - children?: ComponentChildren, - help: ComponentChildren, - attributeName: string, - attributeType: AttributeType, - additionalAttributesToDelete: { type: "label" | "relation", name: string }[] -}) { - return ( - - - {titleIcon && <>{" "}} - {title} - - {children} - - {help && {help}} - { - removeOwnedAttributesByNameOrType(note, attributeType, attributeName); - if (additionalAttributesToDelete) { - for (const { type, name } of additionalAttributesToDelete) { - removeOwnedAttributesByNameOrType(note, type, name); - } - } - }} - /> - - - ) -} - -function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) { - const [ searchString, setSearchString ] = useNoteLabel(note, "searchString"); - const inputRef = useRef(null); - const currentValue = useRef(searchString ?? ""); - const spacedUpdate = useSpacedUpdate(async () => { - const searchString = currentValue.current; - appContext.lastSearchString = searchString; - setSearchString(searchString); - - if (note.title.startsWith(t("search_string.search_prefix"))) { - await server.put(`notes/${note.noteId}/title`, { - title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}` - }); - } - }, 1000); - - // React to errors - const { showTooltip, hideTooltip } = useTooltip(inputRef, { - trigger: "manual", - title: `${t("search_string.error", { error: error?.message })}`, - html: true, - placement: "bottom" - }); - - // Auto-focus. - useEffect(() => inputRef.current?.focus(), []); - - useEffect(() => { - if (error) { - showTooltip(); - setTimeout(() => hideTooltip(), 4000); - } else { - hideTooltip(); - } - }, [ error ]); - - return - {t("search_string.search_syntax")} - {t("search_string.also_see")} {t("search_string.complete_help")} -
    -
  • {t("search_string.full_text_search")}
  • -
  • #abc - {t("search_string.label_abc")}
  • -
  • #year = 2019 - {t("search_string.label_year")}
  • -
  • #rock #pop - {t("search_string.label_rock_pop")}
  • -
  • #rock or #pop - {t("search_string.label_rock_or_pop")}
  • -
  • #year <= 2000 - {t("search_string.label_year_comparison")}
  • -
  • note.dateCreated >= MONTH-1 - {t("search_string.label_date_created")}
  • -
- } - note={note} {...restProps} - > - { - currentValue.current = text; - spacedUpdate.scheduleUpdate(); - }} - onKeyDown={async (e) => { - if (e.key === "Enter") { - e.preventDefault(); - - // this also in effect disallows new lines in query string. - // on one hand, this makes sense since search string is a label - // on the other hand, it could be nice for structuring long search string. It's probably a niche case though. - await spacedUpdate.updateNowIfNecessary(); - refreshResults(); - } - }} - /> -
-} - -function SearchScriptOption({ note, ...restProps }: SearchOptionProps) { - const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript"); - - return -

{t("search_script.description1")}

-

{t("search_script.description2")}

-

{t("search_script.example_title")}

-
{t("search_script.example_code")}
- {t("search_script.note")} - } - note={note} {...restProps} - > - setSearchScript(noteId ?? "root")} - placeholder={t("search_script.placeholder")} - /> -
-} - -function AncestorOption({ note, ...restProps}: SearchOptionProps) { - const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor"); - const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth"); - - const options = useMemo(() => { - const options: { value: string | undefined; label: string }[] = [ - { value: "", label: t("ancestor.depth_doesnt_matter") }, - { value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` } - ]; - - for (let i=2; i<=9; i++) options.push({ value: "eq" + i, label: t("ancestor.depth_eq", { count: i }) }); - for (let i=0; i<=9; i++) options.push({ value: "gt" + i, label: t("ancestor.depth_gt", { count: i }) }); - for (let i=2; i<=9; i++) options.push({ value: "lt" + i, label: t("ancestor.depth_lt", { count: i }) }); - - return options; - }, []); - - return -
- setAncestor(noteId ?? "root")} - placeholder={t("ancestor.placeholder")} - /> - -
{t("ancestor.depth_label")}:
- setDepth(value ? value : null)} - style={{ flexShrink: 3 }} - /> -
-
; -} - -function FastSearchOption({ ...restProps }: SearchOptionProps) { - return -} - -function DebugOption({ ...restProps }: SearchOptionProps) { - return -

{t("debug.debug_info")}

- {t("debug.access_info")} - } - {...restProps} - /> -} - -function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) { - return -} - -function OrderByOption({ note, ...restProps }: SearchOptionProps) { - const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy"); - const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection"); - - return - - {" "} - - -} - -function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) { - const [ limit, setLimit ] = useNoteLabel(note, "limit"); - - return - - -} \ No newline at end of file