From cbaf9e6034aa09911fca967b7af6cad11f154b3e Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Tue, 31 Dec 2024 13:17:56 +0200 Subject: feat: Introduce advanced search capabilities (#753) * feat: Implement search filtering in the backend * feat: Implement search language parser * rename matcher name * Add ability to interleve text * More fixes * be more tolerable to parsing errors * Add a search query explainer widget * Handle date parsing gracefully * Fix the lockfile * Encode query search param * Fix table body error * Fix error when writing quotes --- .../dashboard/search/QueryExplainerTooltip.tsx | 98 ++++++++++++++++++++++ .../components/dashboard/search/SearchInput.tsx | 27 ++++-- 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 apps/web/components/dashboard/search/QueryExplainerTooltip.tsx (limited to 'apps/web/components/dashboard/search') diff --git a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx new file mode 100644 index 00000000..191c9ff3 --- /dev/null +++ b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx @@ -0,0 +1,98 @@ +import InfoTooltip from "@/components/ui/info-tooltip"; +import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; + +import { TextAndMatcher } from "@hoarder/shared/searchQueryParser"; +import { Matcher } from "@hoarder/shared/types/search"; + +export default function QueryExplainerTooltip({ + parsedSearchQuery, + className, +}: { + parsedSearchQuery: TextAndMatcher & { result: string }; + className?: string; +}) { + if (parsedSearchQuery.result == "invalid") { + return null; + } + + const MatcherComp = ({ matcher }: { matcher: Matcher }) => { + switch (matcher.type) { + case "tagName": + return ( + + Tag Name + {matcher.tagName} + + ); + case "listName": + return ( + + List Name + {matcher.listName} + + ); + case "dateAfter": + return ( + + Created After + {matcher.dateAfter.toDateString()} + + ); + case "dateBefore": + return ( + + Created Before + {matcher.dateBefore.toDateString()} + + ); + case "favourited": + return ( + + Favourited + {matcher.favourited.toString()} + + ); + case "archived": + return ( + + Archived + {matcher.archived.toString()} + + ); + case "and": + case "or": + return ( + + {matcher.type} + + + + {matcher.matchers.map((m, i) => ( + + ))} + +
+
+
+ ); + } + }; + + return ( + + + + {parsedSearchQuery.text && ( + + Text + {parsedSearchQuery.text} + + )} + {parsedSearchQuery.matcher && ( + + )} + +
+
+ ); +} diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx index 55f304e3..8ed2ea3c 100644 --- a/apps/web/components/dashboard/search/SearchInput.tsx +++ b/apps/web/components/dashboard/search/SearchInput.tsx @@ -4,6 +4,9 @@ import React, { useEffect, useImperativeHandle, useRef } from "react"; import { Input } from "@/components/ui/input"; import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search"; import { useTranslation } from "@/lib/i18n/client"; +import { cn } from "@/lib/utils"; + +import QueryExplainerTooltip from "./QueryExplainerTooltip"; function useFocusSearchOnKeyPress( inputRef: React.RefObject, @@ -47,7 +50,8 @@ const SearchInput = React.forwardRef< React.HTMLAttributes & { loading?: boolean } >(({ className, ...props }, ref) => { const { t } = useTranslation(); - const { debounceSearch, searchQuery, isInSearchPage } = useDoBookmarkSearch(); + const { debounceSearch, searchQuery, parsedSearchQuery, isInSearchPage } = + useDoBookmarkSearch(); const [value, setValue] = React.useState(searchQuery); @@ -67,14 +71,19 @@ const SearchInput = React.forwardRef< }, [isInSearchPage]); return ( - +
+ + +
); }); SearchInput.displayName = "SearchInput"; -- cgit v1.2.3-70-g09d2