diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 478127bb9..107d66e74 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -889,7 +889,8 @@ "search_parameters": "Search Parameters", "unknown_search_option": "Unknown search option {{searchOptionName}}", "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": { "title": "Similar Notes", diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css index a189480c8..7d1c2f86c 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.css +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -4,29 +4,8 @@ body.experimental-feature-new-layout { } .title-actions { - padding: 0; - display: flex; - gap: 0.25em; - align-items: center; - width: 100%; - max-width: unset; padding-inline-start: 15px; + padding-top: 1em; 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; - } } } diff --git a/apps/client/src/widgets/note_bars/CollectionProperties.css b/apps/client/src/widgets/note_bars/CollectionProperties.css new file mode 100644 index 000000000..c4db1a8ae --- /dev/null +++ b/apps/client/src/widgets/note_bars/CollectionProperties.css @@ -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; + } +} diff --git a/apps/client/src/widgets/note_bars/CollectionProperties.tsx b/apps/client/src/widgets/note_bars/CollectionProperties.tsx index bf05a131e..0c07e8f02 100644 --- a/apps/client/src/widgets/note_bars/CollectionProperties.tsx +++ b/apps/client/src/widgets/note_bars/CollectionProperties.tsx @@ -1,9 +1,14 @@ +import "./CollectionProperties.css"; + import { t } from "i18next"; import { useContext } from "preact/hooks"; import { Fragment } from "preact/jsx-runtime"; import FNote from "../../entities/fnote"; +import { getHelpUrlForNote } from "../../services/in_app_help"; +import { openInAppHelpFromUrl } from "../../services/utils"; import { ViewTypeOptions } from "../collections/interface"; +import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList"; import FormTextBox from "../react/FormTextBox"; @@ -12,9 +17,6 @@ import Icon from "../react/Icon"; import { ParentComponent } from "../react/react_utils"; import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config"; 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 = { grid: "bx bxs-grid", @@ -30,12 +32,12 @@ export default function CollectionProperties({ note }: { note: FNote }) { const [ viewType, setViewType ] = useViewType(note); return ( - <> +
- +
); } @@ -187,9 +189,9 @@ function ComboBoxPropertyView({ note, property }: { note: FNote, property: Combo {index < property.options.length - 1 && } ); - } else { - return renderItem(option); } + return renderItem(option); + })} ); diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts index 59920e017..4960c31c9 100644 --- a/apps/client/src/widgets/ribbon/RibbonDefinition.ts +++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts @@ -60,7 +60,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ title: t("book_properties.book_properties"), icon: "bx bx-book", content: CollectionPropertiesTab, - show: ({ note }) => !isNewLayout && note?.type === "book" || note?.type === "search", + show: ({ note }) => !isNewLayout && (note?.type === "book" || note?.type === "search"), toggleCommand: "toggleRibbonTabBookProperties" }, { diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx index 33f9d0a36..90bea4ce2 100644 --- a/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx +++ b/apps/client/src/widgets/ribbon/SearchDefinitionOptions.tsx @@ -1,360 +1,361 @@ -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 { AttributeType } from "@triliumnext/commons"; import { ComponentChildren, VNode } from "preact"; +import { useEffect, useMemo, useRef } from "preact/hooks"; + +import appContext from "../../components/app_context"; 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 FormSelect from "../react/FormSelect"; +import FormTextArea from "../react/FormTextArea"; +import FormTextBox from "../react/FormTextBox"; 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 { - attributeName: string; - attributeType: "label" | "relation"; - icon: string; - label: string; - tooltip?: string; - component: (props: SearchOptionProps) => VNode; - defaultValue?: string; - additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]; + 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 }; + 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 - } + { + 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 }[] + 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); - } - } - }} - /> - - ) + 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); + 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(); + 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"); + 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")} - /> -
+ 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 [ 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")})` } - ]; + 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 }) }); + 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 options; + }, []); - return -
- setAncestor(noteId ?? "root")} - placeholder={t("ancestor.placeholder")} - /> + return +
+ setAncestor(noteId ?? "root")} + placeholder={t("ancestor.placeholder")} + /> -
{t("ancestor.depth_label")}:
- setDepth(value ? value : null)} - style={{ flexShrink: 3 }} - /> -
-
; +
{t("ancestor.depth_label")}:
+ setDepth(value ? value : null)} + style={{ flexShrink: 3 }} + /> +
+
; } function FastSearchOption({ ...restProps }: SearchOptionProps) { - return + return ; } function DebugOption({ ...restProps }: SearchOptionProps) { - return -

{t("debug.debug_info")}

- {t("debug.access_info")} - } - {...restProps} - /> + return +

{t("debug.debug_info")}

+ {t("debug.access_info")} + } + {...restProps} + />; } function IncludeArchivedNotesOption({ ...restProps }: SearchOptionProps) { - return + return ; } function OrderByOption({ note, ...restProps }: SearchOptionProps) { - const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy"); - const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection"); + const [ orderBy, setOrderBy ] = useNoteLabel(note, "orderBy"); + const [ orderDirection, setOrderDirection ] = useNoteLabel(note, "orderDirection"); - return - - {" "} - - + return + + {" "} + + ; } function LimitOption({ note, defaultValue, ...restProps }: SearchOptionProps) { - const [ limit, setLimit ] = useNoteLabel(note, "limit"); + const [ limit, setLimit ] = useNoteLabel(note, "limit"); - return - - + return + + ; } diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx index 273adbfef..66d283a11 100644 --- a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx +++ b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx @@ -7,6 +7,7 @@ import appContext from "../../components/app_context"; import FNote from "../../entities/fnote"; import attributes from "../../services/attributes"; import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import froca from "../../services/froca"; import { t } from "../../services/i18n"; import server from "../../services/server"; @@ -15,6 +16,7 @@ import tree from "../../services/tree"; import { getErrorMessage } from "../../services/utils"; import ws from "../../services/ws"; import RenameNoteBulkAction from "../bulk_actions/note/rename_note"; +import CollectionProperties from "../note_bars/CollectionProperties"; import Button from "../react/Button"; import Dropdown from "../react/Dropdown"; import { FormListHeader, FormListItem } from "../react/FormList"; @@ -24,6 +26,8 @@ import { ParentComponent } from "../react/react_utils"; import { TabContext } from "./ribbon-interface"; import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions"; +const isNewLayout = isExperimentalFeatureEnabled("new-layout"); + export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick) { const parentComponent = useContext(ParentComponent); const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>(); @@ -78,85 +82,90 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick
- {note && !hidden && - - - - - + + +
{t("search_definition.add_search_option")} - {searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => ( -
+ )}
);