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"; import HelpRemoveButtons from "../react/HelpRemoveButtons"; 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} { 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 }