diff --git a/CHANGELOG.md b/CHANGELOG.md index d487b5b8c..6b0b63350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Upgraded `brace-expansion` to `^1.1.17`/`^2.1.3`/`^5.0.8`. [#1527](https://github.com/sourcebot-dev/sourcebot/pull/1527) +- The search results panel now distinguishes "the raw search returned no results" from "the active filters excluded everything" when the post-filter list is empty. The latter shows a "No results match the active filters." message with a "Clear filters" button that removes the `repos` and `langs` URL query params. [#1532](https://github.com/sourcebot-dev/sourcebot/pull/1532) ## [5.1.5] - 2026-07-31 diff --git a/packages/web/src/app/(app)/search/components/searchResultsPage.tsx b/packages/web/src/app/(app)/search/components/searchResultsPage.tsx index f16ed7726..de488fd02 100644 --- a/packages/web/src/app/(app)/search/components/searchResultsPage.tsx +++ b/packages/web/src/app/(app)/search/components/searchResultsPage.tsx @@ -22,7 +22,7 @@ import { InfoCircledIcon } from "@radix-ui/react-icons"; import { useLocalStorage } from "@uidotdev/usehooks"; import { AlertTriangleIcon, BugIcon, FilterIcon, RefreshCwIcon } from "lucide-react"; import { Session } from "next-auth"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { ImperativePanelHandle } from "react-resizable-panels"; @@ -32,6 +32,8 @@ import { useStreamedSearch } from "../useStreamedSearch"; import { CodePreviewPanel } from "./codePreviewPanel"; import { FilterPanel } from "./filterPanel"; import { useFilteredMatches } from "./filterPanel/useFilterMatches"; +import { useGetSelectedFromQuery } from "./filterPanel/useGetSelectedFromQuery"; +import { LANGUAGES_QUERY_PARAM, REPOS_QUERY_PARAM } from "./filterPanel/useFilterMatches"; import { SearchResultsPanel, SearchResultsPanelHandle } from "./searchResultsPanel"; interface SearchResultsPageProps { @@ -236,6 +238,32 @@ const PanelGroup = ({ const filterPanelRef = useRef(null); const searchResultsPanelRef = useRef(null); const [selectedMatchIndex, setSelectedMatchIndex] = useState(0); + const pathname = usePathname(); + const router = useRouter(); + const searchParams = useSearchParams(); + const { getSelectedFromQuery } = useGetSelectedFromQuery(); + + // True iff the user has at least one repo or language filter + // applied. When the post-filter results are empty, this is what + // distinguishes "the query matched nothing" from "the filters + // excluded everything". See issue #1532. + const hasActiveFilters = useMemo(() => { + return getSelectedFromQuery(REPOS_QUERY_PARAM).size > 0 + || getSelectedFromQuery(LANGUAGES_QUERY_PARAM).size > 0; + }, [getSelectedFromQuery, searchParams]); + + const onClearFilters = useCallback(() => { + // Preserve every other URL param (the search query, regex + // toggle, etc.) and drop only the repo/language filter params. + // The next render re-derives `filteredFileMatches` from the + // now-empty filter set, so the panel re-renders with all raw + // matches visible. + const next = new URLSearchParams(searchParams.toString()); + next.delete(REPOS_QUERY_PARAM); + next.delete(LANGUAGES_QUERY_PARAM); + const qs = next.toString(); + router.replace(qs.length > 0 ? `${pathname}?${qs}` : pathname); + }, [pathname, router, searchParams]); const [isFilterPanelCollapsed, setIsFilterPanelCollapsed] = useLocalStorage('isFilterPanelCollapsed', false); @@ -384,6 +412,24 @@ const PanelGroup = ({

Searching...

+ ) : fileMatches.length > 0 && hasActiveFilters ? ( + // The raw search returned matches but the + // active repo/language filters excluded all + // of them. The user can't tell the two "no + // results" cases apart without a hint, and + // the filter panel is collapsed by default. + // The "Clear filters" button removes the + // `repos` and `langs` URL params; the next + // render re-derives `filteredFileMatches` + // from the now-empty filter set, so the + // panel re-renders with all raw matches. + // Issue #1532. +
+

No results match the active filters.

+ +
) : (

No results found