fix(frontend): suppress ?q= storage restore when ?qs= is present

When `/knowledges` is opened via a `?qs=…` link, `useTableState`'s
mount-time restore was injecting the previous session's `?q=` from
storage on top of it — so a clean semantic-search URL silently became
`?qs=foo&q=python` and the user got a narrowed result set they didn't
ask for.

Added `skipRestore` to `useTableState` (defaults to `false`, captured
via a ref at mount so a later toggle doesn't replay a restore the user
has worked past). `knowledges.tsx` passes `searchParams.has('qs')` so
the restore is skipped exactly when the parameter is part of the
landing URL.

Mirror-to-storage on subsequent filter changes is unaffected — a
client filter the user actually types still persists to storage for
the next session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-05-19 13:16:25 +07:00
co-authored by Claude Opus 4.7
parent 2a4417505a
commit 4430cfc329
2 changed files with 34 additions and 3 deletions
+22
View File
@@ -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
* `?<filterParamName>=` 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 | string>(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
+12 -3
View File
@@ -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<KnowledgeDocType, BadgeVariant> = {
@@ -63,7 +63,16 @@ function Knowledges() {
const [isRenameLoading, setIsRenameLoading] = useState(false);
const editingInputRef = useRef<HTMLInputElement>(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) => {