mirror of
https://github.com/zadam/trilium.git
synced 2025-12-17 12:54:24 +01:00
feat(layout/search_definition): integrate view options directly in search parameters
This commit is contained in:
parent
6b9b9a96c3
commit
a7ca839afb
@ -889,7 +889,8 @@
|
|||||||
"search_parameters": "Search Parameters",
|
"search_parameters": "Search Parameters",
|
||||||
"unknown_search_option": "Unknown search option {{searchOptionName}}",
|
"unknown_search_option": "Unknown search option {{searchOptionName}}",
|
||||||
"search_note_saved": "Search note has been saved into {{- notePathTitle}}",
|
"search_note_saved": "Search note has been saved into {{- notePathTitle}}",
|
||||||
"actions_executed": "Actions have been executed."
|
"actions_executed": "Actions have been executed.",
|
||||||
|
"view_options": "View options:"
|
||||||
},
|
},
|
||||||
"similar_notes": {
|
"similar_notes": {
|
||||||
"title": "Similar Notes",
|
"title": "Similar Notes",
|
||||||
|
|||||||
@ -4,29 +4,8 @@ body.experimental-feature-new-layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title-actions {
|
.title-actions {
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25em;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: unset;
|
|
||||||
padding-inline-start: 15px;
|
padding-inline-start: 15px;
|
||||||
|
padding-top: 1em;
|
||||||
padding-bottom: 0.2em;
|
padding-bottom: 0.2em;
|
||||||
font-size: 0.8em;
|
|
||||||
|
|
||||||
.collapsible-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
input.form-control {
|
|
||||||
padding: 2px 8px;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
apps/client/src/widgets/note_bars/CollectionProperties.css
Normal file
20
apps/client/src/widgets/note_bars/CollectionProperties.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.collection-properties {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25em;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: unset;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
input.form-control {
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,14 @@
|
|||||||
|
import "./CollectionProperties.css";
|
||||||
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
import { Fragment } from "preact/jsx-runtime";
|
import { Fragment } from "preact/jsx-runtime";
|
||||||
|
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
|
import { getHelpUrlForNote } from "../../services/in_app_help";
|
||||||
|
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||||
import { ViewTypeOptions } from "../collections/interface";
|
import { ViewTypeOptions } from "../collections/interface";
|
||||||
|
import ActionButton from "../react/ActionButton";
|
||||||
import Dropdown from "../react/Dropdown";
|
import Dropdown from "../react/Dropdown";
|
||||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||||
import FormTextBox from "../react/FormTextBox";
|
import FormTextBox from "../react/FormTextBox";
|
||||||
@ -12,9 +17,6 @@ import Icon from "../react/Icon";
|
|||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent } from "../react/react_utils";
|
||||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
||||||
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
|
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
|
||||||
import ActionButton from "../react/ActionButton";
|
|
||||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
|
||||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
|
||||||
|
|
||||||
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||||
grid: "bx bxs-grid",
|
grid: "bx bxs-grid",
|
||||||
@ -30,12 +32,12 @@ export default function CollectionProperties({ note }: { note: FNote }) {
|
|||||||
const [ viewType, setViewType ] = useViewType(note);
|
const [ viewType, setViewType ] = useViewType(note);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="collection-properties">
|
||||||
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
|
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
|
||||||
<ViewOptions note={note} viewType={viewType} />
|
<ViewOptions note={note} viewType={viewType} />
|
||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
<HelpButton note={note} />
|
<HelpButton note={note} />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,9 +189,9 @@ function ComboBoxPropertyView({ note, property }: { note: FNote, property: Combo
|
|||||||
{index < property.options.length - 1 && <FormDropdownDivider />}
|
{index < property.options.length - 1 && <FormDropdownDivider />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return renderItem(option);
|
|
||||||
}
|
}
|
||||||
|
return renderItem(option);
|
||||||
|
|
||||||
})}
|
})}
|
||||||
</FormDropdownSubmenu>
|
</FormDropdownSubmenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
|||||||
title: t("book_properties.book_properties"),
|
title: t("book_properties.book_properties"),
|
||||||
icon: "bx bx-book",
|
icon: "bx bx-book",
|
||||||
content: CollectionPropertiesTab,
|
content: CollectionPropertiesTab,
|
||||||
show: ({ note }) => !isNewLayout && note?.type === "book" || note?.type === "search",
|
show: ({ note }) => !isNewLayout && (note?.type === "book" || note?.type === "search"),
|
||||||
toggleCommand: "toggleRibbonTabBookProperties"
|
toggleCommand: "toggleRibbonTabBookProperties"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,360 +1,361 @@
|
|||||||
import FormTextArea from "../react/FormTextArea";
|
import { AttributeType } from "@triliumnext/commons";
|
||||||
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 { ComponentChildren, VNode } from "preact";
|
||||||
|
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||||
|
|
||||||
|
import appContext from "../../components/app_context";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import { removeOwnedAttributesByNameOrType } from "../../services/attributes";
|
import { removeOwnedAttributesByNameOrType } from "../../services/attributes";
|
||||||
import { AttributeType } from "@triliumnext/commons";
|
|
||||||
import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks";
|
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
|
||||||
import appContext from "../../components/app_context";
|
|
||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
|
import FormSelect from "../react/FormSelect";
|
||||||
|
import FormTextArea from "../react/FormTextArea";
|
||||||
|
import FormTextBox from "../react/FormTextBox";
|
||||||
import HelpRemoveButtons from "../react/HelpRemoveButtons";
|
import HelpRemoveButtons from "../react/HelpRemoveButtons";
|
||||||
|
import { useNoteLabel, useNoteRelation, useSpacedUpdate, useTooltip } from "../react/hooks";
|
||||||
|
import Icon from "../react/Icon";
|
||||||
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
|
|
||||||
export interface SearchOption {
|
export interface SearchOption {
|
||||||
attributeName: string;
|
attributeName: string;
|
||||||
attributeType: "label" | "relation";
|
attributeType: "label" | "relation";
|
||||||
icon: string;
|
icon: string;
|
||||||
label: string;
|
label: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
component: (props: SearchOptionProps) => VNode;
|
component: (props: SearchOptionProps) => VNode;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchOptionProps {
|
interface SearchOptionProps {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
refreshResults: () => void;
|
refreshResults: () => void;
|
||||||
attributeName: string;
|
attributeName: string;
|
||||||
attributeType: "label" | "relation";
|
attributeType: "label" | "relation";
|
||||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[];
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
error?: { message: string };
|
error?: { message: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SEARCH_OPTIONS: SearchOption[] = [
|
export const SEARCH_OPTIONS: SearchOption[] = [
|
||||||
{
|
{
|
||||||
attributeName: "searchString",
|
attributeName: "searchString",
|
||||||
attributeType: "label",
|
attributeType: "label",
|
||||||
icon: "bx bx-text",
|
icon: "bx bx-text",
|
||||||
label: t("search_definition.search_string"),
|
label: t("search_definition.search_string"),
|
||||||
component: SearchStringOption
|
component: SearchStringOption
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributeName: "searchScript",
|
attributeName: "searchScript",
|
||||||
attributeType: "relation",
|
attributeType: "relation",
|
||||||
defaultValue: "root",
|
defaultValue: "root",
|
||||||
icon: "bx bx-code",
|
icon: "bx bx-code",
|
||||||
label: t("search_definition.search_script"),
|
label: t("search_definition.search_script"),
|
||||||
component: SearchScriptOption
|
component: SearchScriptOption
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributeName: "ancestor",
|
attributeName: "ancestor",
|
||||||
attributeType: "relation",
|
attributeType: "relation",
|
||||||
defaultValue: "root",
|
defaultValue: "root",
|
||||||
icon: "bx bx-filter-alt",
|
icon: "bx bx-filter-alt",
|
||||||
label: t("search_definition.ancestor"),
|
label: t("search_definition.ancestor"),
|
||||||
component: AncestorOption,
|
component: AncestorOption,
|
||||||
additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ]
|
additionalAttributesToDelete: [ { type: "label", name: "ancestorDepth" } ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributeName: "fastSearch",
|
attributeName: "fastSearch",
|
||||||
attributeType: "label",
|
attributeType: "label",
|
||||||
icon: "bx bx-run",
|
icon: "bx bx-run",
|
||||||
label: t("search_definition.fast_search"),
|
label: t("search_definition.fast_search"),
|
||||||
tooltip: t("search_definition.fast_search_description"),
|
tooltip: t("search_definition.fast_search_description"),
|
||||||
component: FastSearchOption
|
component: FastSearchOption
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributeName: "includeArchivedNotes",
|
attributeName: "includeArchivedNotes",
|
||||||
attributeType: "label",
|
attributeType: "label",
|
||||||
icon: "bx bx-archive",
|
icon: "bx bx-archive",
|
||||||
label: t("search_definition.include_archived"),
|
label: t("search_definition.include_archived"),
|
||||||
tooltip: t("search_definition.include_archived_notes_description"),
|
tooltip: t("search_definition.include_archived_notes_description"),
|
||||||
component: IncludeArchivedNotesOption
|
component: IncludeArchivedNotesOption
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributeName: "orderBy",
|
attributeName: "orderBy",
|
||||||
attributeType: "label",
|
attributeType: "label",
|
||||||
defaultValue: "relevancy",
|
defaultValue: "relevancy",
|
||||||
icon: "bx bx-arrow-from-top",
|
icon: "bx bx-arrow-from-top",
|
||||||
label: t("search_definition.order_by"),
|
label: t("search_definition.order_by"),
|
||||||
component: OrderByOption,
|
component: OrderByOption,
|
||||||
additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ]
|
additionalAttributesToDelete: [ { type: "label", name: "orderDirection" } ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributeName: "limit",
|
attributeName: "limit",
|
||||||
attributeType: "label",
|
attributeType: "label",
|
||||||
defaultValue: "10",
|
defaultValue: "10",
|
||||||
icon: "bx bx-stop",
|
icon: "bx bx-stop",
|
||||||
label: t("search_definition.limit"),
|
label: t("search_definition.limit"),
|
||||||
tooltip: t("search_definition.limit_description"),
|
tooltip: t("search_definition.limit_description"),
|
||||||
component: LimitOption
|
component: LimitOption
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attributeName: "debug",
|
attributeName: "debug",
|
||||||
attributeType: "label",
|
attributeType: "label",
|
||||||
icon: "bx bx-bug",
|
icon: "bx bx-bug",
|
||||||
label: t("search_definition.debug"),
|
label: t("search_definition.debug"),
|
||||||
tooltip: t("search_definition.debug_description"),
|
tooltip: t("search_definition.debug_description"),
|
||||||
component: DebugOption
|
component: DebugOption
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: {
|
function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
title: string,
|
title: string,
|
||||||
titleIcon?: string,
|
titleIcon?: string,
|
||||||
children?: ComponentChildren,
|
children?: ComponentChildren,
|
||||||
help?: ComponentChildren,
|
help?: ComponentChildren,
|
||||||
attributeName: string,
|
attributeName: string,
|
||||||
attributeType: AttributeType,
|
attributeType: AttributeType,
|
||||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]
|
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<tr className={attributeName}>
|
<tr className={attributeName}>
|
||||||
<td className="title-column">
|
<td className="title-column">
|
||||||
{titleIcon && <><Icon icon={titleIcon} />{" "}</>}
|
{titleIcon && <><Icon icon={titleIcon} />{" "}</>}
|
||||||
{title}
|
{title}
|
||||||
</td>
|
</td>
|
||||||
<td>{children}</td>
|
<td>{children}</td>
|
||||||
<HelpRemoveButtons
|
<HelpRemoveButtons
|
||||||
help={help}
|
help={help}
|
||||||
removeText={t("abstract_search_option.remove_this_search_option")}
|
removeText={t("abstract_search_option.remove_this_search_option")}
|
||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
removeOwnedAttributesByNameOrType(note, attributeType, attributeName);
|
removeOwnedAttributesByNameOrType(note, attributeType, attributeName);
|
||||||
if (additionalAttributesToDelete) {
|
if (additionalAttributesToDelete) {
|
||||||
for (const { type, name } of additionalAttributesToDelete) {
|
for (const { type, name } of additionalAttributesToDelete) {
|
||||||
removeOwnedAttributesByNameOrType(note, type, name);
|
removeOwnedAttributesByNameOrType(note, type, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) {
|
function SearchStringOption({ note, refreshResults, error, ...restProps }: SearchOptionProps) {
|
||||||
const [ searchString, setSearchString ] = useNoteLabel(note, "searchString");
|
const [ searchString, setSearchString ] = useNoteLabel(note, "searchString");
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const currentValue = useRef(searchString ?? "");
|
const currentValue = useRef(searchString ?? "");
|
||||||
const spacedUpdate = useSpacedUpdate(async () => {
|
const spacedUpdate = useSpacedUpdate(async () => {
|
||||||
const searchString = currentValue.current;
|
const searchString = currentValue.current;
|
||||||
appContext.lastSearchString = searchString;
|
appContext.lastSearchString = searchString;
|
||||||
setSearchString(searchString);
|
setSearchString(searchString);
|
||||||
|
|
||||||
if (note.title.startsWith(t("search_string.search_prefix"))) {
|
if (note.title.startsWith(t("search_string.search_prefix"))) {
|
||||||
await server.put(`notes/${note.noteId}/title`, {
|
await server.put(`notes/${note.noteId}/title`, {
|
||||||
title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}`
|
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 <SearchOption
|
|
||||||
title={t("search_string.title_column")}
|
|
||||||
help={<>
|
|
||||||
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
|
|
||||||
<ul style="marigin-bottom: 0;">
|
|
||||||
<li>{t("search_string.full_text_search")}</li>
|
|
||||||
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
|
|
||||||
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
|
|
||||||
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
|
|
||||||
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
|
|
||||||
<li><code>#year <= 2000</code> - {t("search_string.label_year_comparison")}</li>
|
|
||||||
<li><code>note.dateCreated >= MONTH-1</code> - {t("search_string.label_date_created")}</li>
|
|
||||||
</ul>
|
|
||||||
</>}
|
|
||||||
note={note} {...restProps}
|
|
||||||
>
|
|
||||||
<FormTextArea
|
|
||||||
inputRef={inputRef}
|
|
||||||
className="search-string"
|
|
||||||
placeholder={t("search_string.placeholder")}
|
|
||||||
currentValue={searchString ?? ""}
|
|
||||||
onChange={text => {
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}}
|
}, 1000);
|
||||||
/>
|
|
||||||
</SearchOption>
|
// 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 <SearchOption
|
||||||
|
title={t("search_string.title_column")}
|
||||||
|
help={<>
|
||||||
|
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
|
||||||
|
<ul style="marigin-bottom: 0;">
|
||||||
|
<li>{t("search_string.full_text_search")}</li>
|
||||||
|
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
|
||||||
|
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
|
||||||
|
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
|
||||||
|
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
|
||||||
|
<li><code>#year <= 2000</code> - {t("search_string.label_year_comparison")}</li>
|
||||||
|
<li><code>note.dateCreated >= MONTH-1</code> - {t("search_string.label_date_created")}</li>
|
||||||
|
</ul>
|
||||||
|
</>}
|
||||||
|
note={note} {...restProps}
|
||||||
|
>
|
||||||
|
<FormTextArea
|
||||||
|
inputRef={inputRef}
|
||||||
|
className="search-string"
|
||||||
|
placeholder={t("search_string.placeholder")}
|
||||||
|
currentValue={searchString ?? ""}
|
||||||
|
onChange={text => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SearchOption>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchScriptOption({ note, ...restProps }: SearchOptionProps) {
|
function SearchScriptOption({ note, ...restProps }: SearchOptionProps) {
|
||||||
const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript");
|
const [ searchScript, setSearchScript ] = useNoteRelation(note, "searchScript");
|
||||||
|
|
||||||
return <SearchOption
|
return <SearchOption
|
||||||
title={t("search_script.title")}
|
title={t("search_script.title")}
|
||||||
help={<>
|
help={<>
|
||||||
<p>{t("search_script.description1")}</p>
|
<p>{t("search_script.description1")}</p>
|
||||||
<p>{t("search_script.description2")}</p>
|
<p>{t("search_script.description2")}</p>
|
||||||
<p>{t("search_script.example_title")}</p>
|
<p>{t("search_script.example_title")}</p>
|
||||||
<pre>{t("search_script.example_code")}</pre>
|
<pre>{t("search_script.example_code")}</pre>
|
||||||
{t("search_script.note")}
|
{t("search_script.note")}
|
||||||
</>}
|
</>}
|
||||||
note={note} {...restProps}
|
note={note} {...restProps}
|
||||||
>
|
>
|
||||||
<NoteAutocomplete
|
<NoteAutocomplete
|
||||||
noteId={searchScript !== "root" ? searchScript ?? undefined : undefined}
|
noteId={searchScript !== "root" ? searchScript ?? undefined : undefined}
|
||||||
noteIdChanged={noteId => setSearchScript(noteId ?? "root")}
|
noteIdChanged={noteId => setSearchScript(noteId ?? "root")}
|
||||||
placeholder={t("search_script.placeholder")}
|
placeholder={t("search_script.placeholder")}
|
||||||
/>
|
/>
|
||||||
</SearchOption>
|
</SearchOption>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AncestorOption({ note, ...restProps}: SearchOptionProps) {
|
function AncestorOption({ note, ...restProps}: SearchOptionProps) {
|
||||||
const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor");
|
const [ ancestor, setAncestor ] = useNoteRelation(note, "ancestor");
|
||||||
const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth");
|
const [ depth, setDepth ] = useNoteLabel(note, "ancestorDepth");
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const options: { value: string | undefined; label: string }[] = [
|
const options: { value: string | undefined; label: string }[] = [
|
||||||
{ value: "", label: t("ancestor.depth_doesnt_matter") },
|
{ value: "", label: t("ancestor.depth_doesnt_matter") },
|
||||||
{ value: "eq1", label: `${t("ancestor.depth_eq", { count: 1 })} (${t("ancestor.direct_children")})` }
|
{ 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=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=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 }) });
|
for (let i=2; i<=9; i++) options.push({ value: `lt${ i}`, label: t("ancestor.depth_lt", { count: i }) });
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <SearchOption
|
return <SearchOption
|
||||||
title={t("ancestor.label")}
|
title={t("ancestor.label")}
|
||||||
note={note} {...restProps}
|
note={note} {...restProps}
|
||||||
>
|
>
|
||||||
<div style={{display: "flex", alignItems: "center"}}>
|
<div style={{display: "flex", alignItems: "center"}}>
|
||||||
<NoteAutocomplete
|
<NoteAutocomplete
|
||||||
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
|
noteId={ancestor !== "root" ? ancestor ?? undefined : undefined}
|
||||||
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
|
noteIdChanged={noteId => setAncestor(noteId ?? "root")}
|
||||||
placeholder={t("ancestor.placeholder")}
|
placeholder={t("ancestor.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style="margin-inline-start: 10px; margin-inline-end: 10px">{t("ancestor.depth_label")}:</div>
|
<div style="margin-inline-start: 10px; margin-inline-end: 10px">{t("ancestor.depth_label")}:</div>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
values={options}
|
values={options}
|
||||||
keyProperty="value" titleProperty="label"
|
keyProperty="value" titleProperty="label"
|
||||||
currentValue={depth ?? ""} onChange={(value) => setDepth(value ? value : null)}
|
currentValue={depth ?? ""} onChange={(value) => setDepth(value ? value : null)}
|
||||||
style={{ flexShrink: 3 }}
|
style={{ flexShrink: 3 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SearchOption>;
|
</SearchOption>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FastSearchOption({ ...restProps }: SearchOptionProps) {
|
function FastSearchOption({ ...restProps }: SearchOptionProps) {
|
||||||
return <SearchOption
|
return <SearchOption
|
||||||
titleIcon="bx bx-run" title={t("fast_search.fast_search")}
|
titleIcon="bx bx-run" title={t("fast_search.fast_search")}
|
||||||
help={t("fast_search.description")}
|
help={t("fast_search.description")}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DebugOption({ ...restProps }: SearchOptionProps) {
|
function DebugOption({ ...restProps }: SearchOptionProps) {
|
||||||
return <SearchOption
|
return <SearchOption
|
||||||
titleIcon="bx bx-bug" title={t("debug.debug")}
|
titleIcon="bx bx-bug" title={t("debug.debug")}
|
||||||
help={<>
|
help={<>
|
||||||
<p>{t("debug.debug_info")}</p>
|
<p>{t("debug.debug_info")}</p>
|
||||||
{t("debug.access_info")}
|
{t("debug.access_info")}
|
||||||
</>}
|
</>}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) {
|
function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) {
|
||||||
return <SearchOption
|
return <SearchOption
|
||||||
titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")}
|
titleIcon="bx bx-archive" title={t("include_archived_notes.include_archived_notes")}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrderByOption({ note, ...restProps }: SearchOptionProps) {
|
function OrderByOption({ note, ...restProps }: SearchOptionProps) {
|
||||||
const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy");
|
const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy");
|
||||||
const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection");
|
const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection");
|
||||||
|
|
||||||
return <SearchOption
|
return <SearchOption
|
||||||
titleIcon="bx bx-arrow-from-top"
|
titleIcon="bx bx-arrow-from-top"
|
||||||
title={t("order_by.order_by")}
|
title={t("order_by.order_by")}
|
||||||
note={note} {...restProps}
|
note={note} {...restProps}
|
||||||
>
|
>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
className="w-auto d-inline"
|
className="w-auto d-inline"
|
||||||
currentValue={orderBy ?? "relevancy"} onChange={setOrderBy}
|
currentValue={orderBy ?? "relevancy"} onChange={setOrderBy}
|
||||||
keyProperty="value" titleProperty="title"
|
keyProperty="value" titleProperty="title"
|
||||||
values={[
|
values={[
|
||||||
{ value: "relevancy", title: t("order_by.relevancy") },
|
{ value: "relevancy", title: t("order_by.relevancy") },
|
||||||
{ value: "title", title: t("order_by.title") },
|
{ value: "title", title: t("order_by.title") },
|
||||||
{ value: "dateCreated", title: t("order_by.date_created") },
|
{ value: "dateCreated", title: t("order_by.date_created") },
|
||||||
{ value: "dateModified", title: t("order_by.date_modified") },
|
{ value: "dateModified", title: t("order_by.date_modified") },
|
||||||
{ value: "contentSize", title: t("order_by.content_size") },
|
{ value: "contentSize", title: t("order_by.content_size") },
|
||||||
{ value: "contentAndAttachmentsSize", title: t("order_by.content_and_attachments_size") },
|
{ value: "contentAndAttachmentsSize", title: t("order_by.content_and_attachments_size") },
|
||||||
{ value: "contentAndAttachmentsAndRevisionsSize", title: t("order_by.content_and_attachments_and_revisions_size") },
|
{ value: "contentAndAttachmentsAndRevisionsSize", title: t("order_by.content_and_attachments_and_revisions_size") },
|
||||||
{ value: "revisionCount", title: t("order_by.revision_count") },
|
{ value: "revisionCount", title: t("order_by.revision_count") },
|
||||||
{ value: "childrenCount", title: t("order_by.children_count") },
|
{ value: "childrenCount", title: t("order_by.children_count") },
|
||||||
{ value: "parentCount", title: t("order_by.parent_count") },
|
{ value: "parentCount", title: t("order_by.parent_count") },
|
||||||
{ value: "ownedLabelCount", title: t("order_by.owned_label_count") },
|
{ value: "ownedLabelCount", title: t("order_by.owned_label_count") },
|
||||||
{ value: "ownedRelationCount", title: t("order_by.owned_relation_count") },
|
{ value: "ownedRelationCount", title: t("order_by.owned_relation_count") },
|
||||||
{ value: "targetRelationCount", title: t("order_by.target_relation_count") },
|
{ value: "targetRelationCount", title: t("order_by.target_relation_count") },
|
||||||
{ value: "random", title: t("order_by.random") }
|
{ value: "random", title: t("order_by.random") }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{" "}
|
{" "}
|
||||||
<FormSelect
|
<FormSelect
|
||||||
className="w-auto d-inline"
|
className="w-auto d-inline"
|
||||||
currentValue={orderDirection ?? "asc"} onChange={setOrderDirection}
|
currentValue={orderDirection ?? "asc"} onChange={setOrderDirection}
|
||||||
keyProperty="value" titleProperty="title"
|
keyProperty="value" titleProperty="title"
|
||||||
values={[
|
values={[
|
||||||
{ value: "asc", title: t("order_by.asc") },
|
{ value: "asc", title: t("order_by.asc") },
|
||||||
{ value: "desc", title: t("order_by.desc") }
|
{ value: "desc", title: t("order_by.desc") }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</SearchOption>
|
</SearchOption>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) {
|
function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) {
|
||||||
const [ limit, setLimit ] = useNoteLabel(note, "limit");
|
const [ limit, setLimit ] = useNoteLabel(note, "limit");
|
||||||
|
|
||||||
return <SearchOption
|
return <SearchOption
|
||||||
titleIcon="bx bx-stop"
|
titleIcon="bx bx-stop"
|
||||||
title={t("limit.limit")}
|
title={t("limit.limit")}
|
||||||
help={t("limit.take_first_x_results")}
|
help={t("limit.take_first_x_results")}
|
||||||
note={note} {...restProps}
|
note={note} {...restProps}
|
||||||
>
|
>
|
||||||
<FormTextBox
|
<FormTextBox
|
||||||
type="number" min="1" step="1"
|
type="number" min="1" step="1"
|
||||||
currentValue={limit ?? defaultValue} onChange={setLimit}
|
currentValue={limit ?? defaultValue} onChange={setLimit}
|
||||||
/>
|
/>
|
||||||
</SearchOption>
|
</SearchOption>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import appContext from "../../components/app_context";
|
|||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import attributes from "../../services/attributes";
|
import attributes from "../../services/attributes";
|
||||||
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
|
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
|
||||||
|
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import server from "../../services/server";
|
import server from "../../services/server";
|
||||||
@ -15,6 +16,7 @@ import tree from "../../services/tree";
|
|||||||
import { getErrorMessage } from "../../services/utils";
|
import { getErrorMessage } from "../../services/utils";
|
||||||
import ws from "../../services/ws";
|
import ws from "../../services/ws";
|
||||||
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
||||||
|
import CollectionProperties from "../note_bars/CollectionProperties";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import Dropdown from "../react/Dropdown";
|
import Dropdown from "../react/Dropdown";
|
||||||
import { FormListHeader, FormListItem } from "../react/FormList";
|
import { FormListHeader, FormListItem } from "../react/FormList";
|
||||||
@ -24,6 +26,8 @@ import { ParentComponent } from "../react/react_utils";
|
|||||||
import { TabContext } from "./ribbon-interface";
|
import { TabContext } from "./ribbon-interface";
|
||||||
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
|
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
|
||||||
|
|
||||||
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
|
|
||||||
export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabContext, "note" | "ntxId" | "hidden">) {
|
export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabContext, "note" | "ntxId" | "hidden">) {
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
|
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
|
||||||
@ -78,85 +82,90 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
|
|||||||
return (
|
return (
|
||||||
<div className="search-definition-widget">
|
<div className="search-definition-widget">
|
||||||
<div className="search-settings">
|
<div className="search-settings">
|
||||||
{note && !hidden &&
|
{note && !hidden && (
|
||||||
<table className="search-setting-table">
|
<table className="search-setting-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="title-column">{t("search_definition.add_search_option")}</td>
|
<td className="title-column">{t("search_definition.add_search_option")}</td>
|
||||||
<td colSpan={2} className="add-search-option">
|
<td colSpan={2} className="add-search-option">
|
||||||
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
text={label}
|
text={label}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<AddBulkActionButton note={note} />
|
<AddBulkActionButton note={note} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tbody className="search-options">
|
<tbody className="search-options">
|
||||||
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
|
{searchOptions?.activeOptions.map(({ attributeType, attributeName, component, additionalAttributesToDelete, defaultValue }) => {
|
||||||
const Component = component;
|
const Component = component;
|
||||||
return <Component
|
return <Component
|
||||||
attributeName={attributeName}
|
attributeName={attributeName}
|
||||||
attributeType={attributeType}
|
attributeType={attributeType}
|
||||||
note={note}
|
note={note}
|
||||||
refreshResults={refreshResults}
|
refreshResults={refreshResults}
|
||||||
error={error}
|
error={error}
|
||||||
additionalAttributesToDelete={additionalAttributesToDelete}
|
additionalAttributesToDelete={additionalAttributesToDelete}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
/>;
|
/>;
|
||||||
})}
|
})}
|
||||||
</tbody>
|
|
||||||
<BulkActionsList note={note} />
|
|
||||||
<tbody className="search-actions">
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3}>
|
|
||||||
<div className="search-actions-container">
|
|
||||||
<Button
|
|
||||||
icon="bx bx-search"
|
|
||||||
text={t("search_definition.search_button")}
|
|
||||||
keyboardShortcut="Enter"
|
|
||||||
onClick={refreshResults}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
{isNewLayout && <tr className="view-options">
|
||||||
icon="bx bxs-zap"
|
<td className="title-column">{t("search_definition.view_options")}</td>
|
||||||
text={t("search_definition.search_execute")}
|
<td><CollectionProperties note={note} /></td>
|
||||||
onClick={async () => {
|
</tr>}
|
||||||
await server.post(`search-and-execute-note/${note.noteId}`);
|
</tbody>
|
||||||
refreshResults();
|
<BulkActionsList note={note} />
|
||||||
toast.showMessage(t("search_definition.actions_executed"), 3000);
|
<tbody className="search-actions">
|
||||||
}}
|
<tr>
|
||||||
/>
|
<td colSpan={3}>
|
||||||
|
<div className="search-actions-container">
|
||||||
|
<Button
|
||||||
|
icon="bx bx-search"
|
||||||
|
text={t("search_definition.search_button")}
|
||||||
|
keyboardShortcut="Enter"
|
||||||
|
onClick={refreshResults}
|
||||||
|
/>
|
||||||
|
|
||||||
{note.isHiddenCompletely() && <Button
|
<Button
|
||||||
icon="bx bx-save"
|
icon="bx bxs-zap"
|
||||||
text={t("search_definition.save_to_note")}
|
text={t("search_definition.search_execute")}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
|
await server.post(`search-and-execute-note/${note.noteId}`);
|
||||||
if (!notePath) {
|
refreshResults();
|
||||||
return;
|
toast.showMessage(t("search_definition.actions_executed"), 3000);
|
||||||
}
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
{note.isHiddenCompletely() && <Button
|
||||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
icon="bx bx-save"
|
||||||
|
text={t("search_definition.save_to_note")}
|
||||||
|
onClick={async () => {
|
||||||
|
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
|
||||||
|
if (!notePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
// See https://www.i18next.com/translation-function/interpolation#unescape
|
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||||
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
|
|
||||||
}}
|
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
|
||||||
/>}
|
// See https://www.i18next.com/translation-function/interpolation#unescape
|
||||||
</div>
|
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
|
||||||
</td>
|
}}
|
||||||
</tr>
|
/>}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</td>
|
||||||
}
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user