From b05a7531b76d580fc2378d3fed12f57e5f4b35b1 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 8 Feb 2026 15:53:14 +0000 Subject: feat: add source filter to query language (#2465) * feat: add source filter to query language Co-Authored-By: Claude Opus 4.6 * autocomplete source --------- Co-authored-by: Claude Opus 4.6 --- .../dashboard/search/QueryExplainerTooltip.tsx | 11 ++++ .../dashboard/search/useSearchAutocomplete.ts | 58 +++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) (limited to 'apps/web/components/dashboard') diff --git a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx index 15facb2d..4d3a690b 100644 --- a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx +++ b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx @@ -208,6 +208,17 @@ export default function QueryExplainerTooltip({ ); + case "source": + return ( + + + {matcher.inverse + ? t("search.is_not_from_source") + : t("search.is_from_source")} + + {matcher.source} + + ); default: { const _exhaustiveCheck: never = matcher; return null; diff --git a/apps/web/components/dashboard/search/useSearchAutocomplete.ts b/apps/web/components/dashboard/search/useSearchAutocomplete.ts index 426e0835..f98f23f5 100644 --- a/apps/web/components/dashboard/search/useSearchAutocomplete.ts +++ b/apps/web/components/dashboard/search/useSearchAutocomplete.ts @@ -4,6 +4,7 @@ import type { LucideIcon } from "lucide-react"; import { useCallback, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { + Globe, History, ListTree, RssIcon, @@ -15,6 +16,7 @@ import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists"; import { useTagAutocomplete } from "@karakeep/shared-react/hooks/tags"; import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; import { useTRPC } from "@karakeep/shared-react/trpc"; +import { zBookmarkSourceSchema } from "@karakeep/shared/types/bookmarks"; const MAX_DISPLAY_SUGGESTIONS = 5; @@ -98,10 +100,14 @@ const QUALIFIER_DEFINITIONS = [ value: "age:", descriptionKey: "search.created_within", }, + { + value: "source:", + descriptionKey: "search.is_from_source", + }, ] satisfies ReadonlyArray; export interface AutocompleteSuggestionItem { - type: "token" | "tag" | "list" | "feed"; + type: "token" | "tag" | "list" | "feed" | "source"; id: string; label: string; insertText: string; @@ -398,6 +404,46 @@ const useListSuggestions = ( return listSuggestions; }; +const SOURCE_VALUES = zBookmarkSourceSchema.options; + +const useSourceSuggestions = ( + parsed: ParsedSearchState, +): AutocompleteSuggestionItem[] => { + const shouldSuggestSources = + parsed.normalizedTokenWithoutMinus.startsWith("source:"); + const sourceSearchTerm = shouldSuggestSources + ? parsed.normalizedTokenWithoutMinus.slice("source:".length) + : ""; + + const sourceSuggestions = useMemo(() => { + if (!shouldSuggestSources) { + return []; + } + + return SOURCE_VALUES.filter((source) => { + if (sourceSearchTerm.length === 0) { + return true; + } + return source.startsWith(sourceSearchTerm); + }) + .slice(0, MAX_DISPLAY_SUGGESTIONS) + .map((source) => { + const insertText = `${parsed.isTokenNegative ? "-" : ""}source:${source}`; + return { + type: "source" as const, + id: `source-${source}`, + label: insertText, + insertText, + appendSpace: true, + description: undefined, + Icon: Globe, + } satisfies AutocompleteSuggestionItem; + }); + }, [shouldSuggestSources, sourceSearchTerm, parsed.isTokenNegative]); + + return sourceSuggestions; +}; + const useHistorySuggestions = ( value: string, history: string[], @@ -440,6 +486,7 @@ export const useSearchAutocomplete = ({ const tagSuggestions = useTagSuggestions(parsedState); const listSuggestions = useListSuggestions(parsedState); const feedSuggestions = useFeedSuggestions(parsedState); + const sourceSuggestions = useSourceSuggestions(parsedState); const historyItems = useHistorySuggestions(value, history); const { activeToken, getActiveToken } = parsedState; @@ -470,6 +517,14 @@ export const useSearchAutocomplete = ({ }); } + if (sourceSuggestions.length > 0) { + groups.push({ + id: "sources", + label: t("search.is_from_source"), + items: sourceSuggestions, + }); + } + // Only suggest qualifiers if no other suggestions are available if (groups.length === 0 && qualifierSuggestions.length > 0) { groups.push({ @@ -493,6 +548,7 @@ export const useSearchAutocomplete = ({ tagSuggestions, listSuggestions, feedSuggestions, + sourceSuggestions, historyItems, t, ]); -- cgit v1.2.3-70-g09d2