chore(react/ribbon): port ancestor depth

This commit is contained in:
Elian Doran 2025-08-24 18:02:18 +03:00
parent 4b212232c8
commit 3bccbabe53
No known key found for this signature in database
3 changed files with 63 additions and 103 deletions

View File

@ -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<string | null | undefined>(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]);

View File

@ -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) {
</td>
</tr>
<tbody className="search-options">
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component }) => {
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete }) => {
return component?.({
attributeName,
attributeType,
note,
refreshResults,
error
error,
additionalAttributesToDelete
});
})}
</tbody>
@ -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 (
<tr>
@ -247,7 +253,14 @@ function SearchOption({ note, title, children, help, attributeName, attributeTyp
<ActionButton
icon="bx bx-x"
className="search-option-del"
onClick={() => removeOwnedAttributesByNameOrType(note, attributeType, attributeName)}
onClick={() => {
removeOwnedAttributesByNameOrType(note, attributeType, attributeName);
if (additionalAttributesToDelete) {
for (const { type, name } of additionalAttributesToDelete) {
removeOwnedAttributesByNameOrType(note, type, name);
}
}
}}
/>
</td>
</tr>
@ -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 <SearchOption
title={t("ancestor.label")}
note={note} {...restProps}
>
<NoteAutocomplete
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
placeholder={t("ancestor.placeholder")}
/>
<div style={{display: "flex", alignItems: "center"}}>
<NoteAutocomplete
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
placeholder={t("ancestor.placeholder")}
/>
<div style="margin-left: 10px; margin-right: 10px">{t("ancestor.depth_label")}:</div>
<FormSelect
values={options}
keyProperty="value" titleProperty="label"
currentValue={depth ?? ""} onChange={(value) => setDepth(value ? value : null)}
style={{ flexShrink: 3 }}
/>
</div>
</SearchOption>;
}

View File

@ -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*/`
<tr>
<td colspan="2">
<div style="margin-left: 10px; margin-right: 10px">${t("ancestor.depth_label")}:</div>
<select name="depth" class="form-select d-inline ancestor-depth" style="flex-shrink: 3">
<option value="">${t("ancestor.depth_doesnt_matter")}</option>
<option value="eq1">${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})</option>
<option value="eq2">${t("ancestor.depth_eq", { count: 2 })}</option>
<option value="eq3">${t("ancestor.depth_eq", { count: 3 })}</option>
<option value="eq4">${t("ancestor.depth_eq", { count: 4 })}</option>
<option value="eq5">${t("ancestor.depth_eq", { count: 5 })}</option>
<option value="eq6">${t("ancestor.depth_eq", { count: 6 })}</option>
<option value="eq7">${t("ancestor.depth_eq", { count: 7 })}</option>
<option value="eq8">${t("ancestor.depth_eq", { count: 8 })}</option>
<option value="eq9">${t("ancestor.depth_eq", { count: 9 })}</option>
<option value="gt0">${t("ancestor.depth_gt", { count: 0 })}</option>
<option value="gt1">${t("ancestor.depth_gt", { count: 1 })}</option>
<option value="gt2">${t("ancestor.depth_gt", { count: 2 })}</option>
<option value="gt3">${t("ancestor.depth_gt", { count: 3 })}</option>
<option value="gt4">${t("ancestor.depth_gt", { count: 4 })}</option>
<option value="gt5">${t("ancestor.depth_gt", { count: 5 })}</option>
<option value="gt6">${t("ancestor.depth_gt", { count: 6 })}</option>
<option value="gt7">${t("ancestor.depth_gt", { count: 7 })}</option>
<option value="gt8">${t("ancestor.depth_gt", { count: 8 })}</option>
<option value="gt9">${t("ancestor.depth_gt", { count: 9 })}</option>
<option value="lt2">${t("ancestor.depth_lt", { count: 2 })}</option>
<option value="lt3">${t("ancestor.depth_lt", { count: 3 })}</option>
<option value="lt4">${t("ancestor.depth_lt", { count: 4 })}</option>
<option value="lt5">${t("ancestor.depth_lt", { count: 5 })}</option>
<option value="lt6">${t("ancestor.depth_lt", { count: 6 })}</option>
<option value="lt7">${t("ancestor.depth_lt", { count: 7 })}</option>
<option value="lt8">${t("ancestor.depth_lt", { count: 8 })}</option>
<option value="lt9">${t("ancestor.depth_lt", { count: 9 })}</option>
</select>
</div>
</td>
</tr>`;
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();
}
}