diff --git a/frontend/src/hooks/use-table-state.ts b/frontend/src/hooks/use-table-state.ts index 3507963e..e2d84b04 100644 --- a/frontend/src/hooks/use-table-state.ts +++ b/frontend/src/hooks/use-table-state.ts @@ -36,6 +36,18 @@ interface UseTableStateOptions { filterParamName?: string; /** Query string param for the 1-based page number. Default `URL_PARAMS.PAGE` (`'page'`). */ pageParamName?: string; + /** + * Suppresses the one-time storage → URL restore on mount. Use on pages + * that route the `?q=` filter alongside a sibling URL parameter with + * its own filter semantics — e.g. `/knowledges` combines `?q=` (client + * text filter) with `?qs=` (server semantic search). Restoring a + * previously-typed `?q=` on top of a freshly-opened `?qs=` link would + * silently narrow the search result the user navigated to. The caller + * decides per-mount whether restore should fire; the value is captured + * via a ref at mount time so a later toggle doesn't replay a restore + * the user has since worked past. + */ + skipRestore?: boolean; /** * Stable storage key for persisting the filter value (a fresh tab without * `?=` resumes from here). Defaults to @@ -86,6 +98,7 @@ export function useTableState(options: UseTableStateOptions = {}): UseTableState debounceMs = 200, filterParamName = URL_PARAMS.QUERY, pageParamName = URL_PARAMS.PAGE, + skipRestore = false, storageKey: explicitStorageKey, } = options; @@ -154,6 +167,11 @@ export function useTableState(options: UseTableStateOptions = {}): UseTableState // exactly once per storageKey rotation — `restoredForKeyReference` // guards against repeating the replay on every render. const restoredForKeyReference = useRef(null); + // Snapshot the `skipRestore` decision at mount via a ref so a later + // toggle (e.g. the user clearing `?qs=`) doesn't fire the restore + // mid-session — by then they've already engaged with the page and a + // surprise URL mutation would be jarring. + const skipRestoreRef = useRef(skipRestore); useEffect(() => { if (restoredForKeyReference.current === storageKey) { @@ -162,6 +180,10 @@ export function useTableState(options: UseTableStateOptions = {}): UseTableState restoredForKeyReference.current = storageKey; + if (skipRestoreRef.current) { + return; + } + if (filter.length > 0) { // URL already has a value — that beats storage. Mirror it back // into storage so a fresh tab without `?q=` resumes from this diff --git a/frontend/src/pages/knowledges/knowledges.tsx b/frontend/src/pages/knowledges/knowledges.tsx index aefbd140..308707ac 100644 --- a/frontend/src/pages/knowledges/knowledges.tsx +++ b/frontend/src/pages/knowledges/knowledges.tsx @@ -2,7 +2,7 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Ellipsis, LibraryBig, Loader2, Pencil, PencilLine, Plus, Trash } from 'lucide-react'; import { useCallback, useRef, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import type { BadgeVariant } from '@/components/ui/badge'; @@ -27,7 +27,7 @@ import { SidebarTrigger } from '@/components/ui/sidebar'; import { StatusCard } from '@/components/ui/status-card'; import { KnowledgeDocType } from '@/graphql/types'; import { useTableState } from '@/hooks/use-table-state'; -import { mergeHrefWithSearchParams } from '@/lib/url-params'; +import { mergeHrefWithSearchParams, URL_PARAMS } from '@/lib/url-params'; import { type Knowledge, useKnowledges } from '@/providers/knowledges-provider'; const docTypeBadgeVariant: Record = { @@ -63,7 +63,16 @@ function Knowledges() { const [isRenameLoading, setIsRenameLoading] = useState(false); const editingInputRef = useRef(null); - const { filter, setFilter } = useTableState(); + // When the user lands on a `?qs=` link (server semantic search) we + // must suppress the storage→URL `?q=` restore. Re-injecting a previous + // session's client filter on top of a fresh semantic-search URL would + // silently narrow the result set behind the user's back. + // `useTableState` reads `skipRestore` once at mount via a ref, so a + // later toggle (user clears `?qs=`) won't replay the restore. + const [initialParams] = useSearchParams(); + const { filter, setFilter } = useTableState({ + skipRestore: initialParams.has(URL_PARAMS.SEARCH), + }); const handleOpen = useCallback( (id: string) => {