From 3bccbabe53eb158f7e06c08d5b502471332c52f8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 24 Aug 2025 18:02:18 +0300 Subject: [PATCH] chore(react/ribbon): port ancestor depth --- apps/client/src/widgets/react/hooks.tsx | 17 +++- .../widgets/ribbon/SearchDefinitionTab.tsx | 61 ++++++++++--- .../src/widgets/search_options/ancestor.ts | 88 ------------------- 3 files changed, 63 insertions(+), 103 deletions(-) delete mode 100644 apps/client/src/widgets/search_options/ancestor.ts diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index be2842759..ad08893e1 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -369,7 +369,14 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: st ] as const; } -export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string) => void] { +/** + * Allows a React component to read or write a note's label while also reacting to changes in value. + * + * @param note the note whose label to read/write. + * @param labelName the name of the label to read/write. + * @returns an array where the first element is the getter and the second element is the setter. The setter has a special behaviour for convenience: if the value is undefined, the label is created without a value (e.g. a tag), if the value is null then the label is removed. + */ +export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string | null | undefined) => void] { const [ labelValue, setLabelValue ] = useState(note?.getLabelValue(labelName)); useEffect(() => setLabelValue(note?.getLabelValue(labelName) ?? null), [ note ]); @@ -381,9 +388,13 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: string): } }); - const setter = useCallback((value: string | undefined) => { + const setter = useCallback((value: string | null | undefined) => { if (note) { - attributes.setLabel(note.noteId, labelName, value) + if (value || value === undefined) { + attributes.setLabel(note.noteId, labelName, value) + } else if (value === null) { + attributes.removeOwnedLabelByName(note, labelName); + } } }, [note]); diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx index f782b31a3..44c9c675e 100644 --- a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx +++ b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx @@ -10,7 +10,7 @@ import attributes, { removeOwnedAttributesByNameOrType } from "../../services/at import FNote from "../../entities/fnote"; import toast from "../../services/toast"; import froca from "../../services/froca"; -import { useContext, useEffect, useRef, useState } from "preact/hooks"; +import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { ParentComponent } from "../react/react_utils"; import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip, useTriliumEventBeta } from "../react/hooks"; import appContext from "../../components/app_context"; @@ -18,6 +18,7 @@ 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"; interface SearchOption { attributeName: string; @@ -27,6 +28,7 @@ interface SearchOption { tooltip?: string; // TODO: Make mandatory once all components are ported. component?: (props: SearchOptionProps) => VNode; + additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; } interface SearchOptionProps { @@ -34,6 +36,7 @@ interface SearchOptionProps { refreshResults: () => void; attributeName: string; attributeType: "label" | "relation"; + additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; error?: { message: string }; } @@ -57,7 +60,8 @@ const SEARCH_OPTIONS: SearchOption[] = [ attributeType: "relation", icon: "bx bx-filter-alt", label: t("search_definition.ancestor"), - component: AncestorOption + component: AncestorOption, + additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ] }, { attributeName: "fastSearch", @@ -168,13 +172,14 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) { - {searchOptions?.activeOptions.map(({ attributeType, attributeName, component }) => { + {searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete }) => { return component?.({ attributeName, attributeType, note, refreshResults, - error + error, + additionalAttributesToDelete }); })} @@ -230,13 +235,14 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) { ) } -function SearchOption({ note, title, children, help, attributeName, attributeType }: { +function SearchOption({ note, title, children, help, attributeName, attributeType, additionalAttributesToDelete }: { note: FNote; title: string, children: ComponentChildren, help: ComponentChildren, attributeName: string, - attributeType: AttributeType + attributeType: AttributeType, + additionalAttributesToDelete: { type: "label" | "relation", name: string }[] }) { return ( @@ -247,7 +253,14 @@ function SearchOption({ note, title, children, help, attributeName, attributeTyp removeOwnedAttributesByNameOrType(note, attributeType, attributeName)} + onClick={() => { + removeOwnedAttributesByNameOrType(note, attributeType, attributeName); + if (additionalAttributesToDelete) { + for (const { type, name } of additionalAttributesToDelete) { + removeOwnedAttributesByNameOrType(note, type, name); + } + } + }} /> @@ -354,15 +367,39 @@ function SearchScriptOption({ note, ...restProps }: SearchOptionProps) { 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")} - /> +
+ setAncestor(noteId ?? "root")} + placeholder={t("ancestor.placeholder")} + /> + +
{t("ancestor.depth_label")}:
+ setDepth(value ? value : null)} + style={{ flexShrink: 3 }} + /> +
; } \ No newline at end of file diff --git a/apps/client/src/widgets/search_options/ancestor.ts b/apps/client/src/widgets/search_options/ancestor.ts deleted file mode 100644 index 54a8874c0..000000000 --- a/apps/client/src/widgets/search_options/ancestor.ts +++ /dev/null @@ -1,88 +0,0 @@ -import AbstractSearchOption from "./abstract_search_option.js"; -import noteAutocompleteService from "../../services/note_autocomplete.js"; -import { t } from "../../services/i18n.js"; - -const TPL = /*html*/` - - -
${t("ancestor.depth_label")}:
- - - -`; - -export default class Ancestor extends AbstractSearchOption { - doRender() { - const $option = $(TPL); - const $ancestor = $option.find(".ancestor"); - const $ancestorDepth = $option.find(".ancestor-depth"); - noteAutocompleteService.initNoteAutocomplete($ancestor); - - $ancestor.on("autocomplete:closed", async () => { - const ancestorNoteId = $ancestor.getSelectedNoteId(); - - if (ancestorNoteId) { - await this.setAttribute("relation", "ancestor", ancestorNoteId); - } - }); - - $ancestorDepth.on("change", async () => { - const ancestorDepth = String($ancestorDepth.val()); - - if (ancestorDepth) { - await this.setAttribute("label", "ancestorDepth", ancestorDepth); - } else { - await this.deleteAttribute("label", "ancestorDepth"); - } - }); - - const ancestorNoteId = this.note.getRelationValue("ancestor"); - - if (ancestorNoteId && ancestorNoteId !== "root") { - $ancestor.setNote(ancestorNoteId); - } - - const ancestorDepth = this.note.getLabelValue("ancestorDepth"); - - if (ancestorDepth) { - $ancestorDepth.val(ancestorDepth); - } - - return $option; - } - - async deleteOption() { - await this.deleteAttribute("label", "ancestorDepth"); - - await super.deleteOption(); - } -}