From a392a72960d782ac2ffa04525dc979df6028939a Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 29 May 2026 15:30:35 +0800 Subject: [PATCH] chore: not store search tag condition in url (#36814) --- .../apps/app-list-browsing-flow.test.tsx | 1 + web/__tests__/apps/create-app-flow.test.tsx | 1 + .../components/apps/__tests__/list.spec.tsx | 34 +++++++++++++++---- .../__tests__/use-apps-query-state.spec.tsx | 31 +---------------- .../apps/hooks/use-apps-query-state.ts | 12 ++----- web/app/components/apps/list.tsx | 18 ++++++++-- 6 files changed, 48 insertions(+), 49 deletions(-) diff --git a/web/__tests__/apps/app-list-browsing-flow.test.tsx b/web/__tests__/apps/app-list-browsing-flow.test.tsx index 4829adacf0..b16f7a2bc0 100644 --- a/web/__tests__/apps/app-list-browsing-flow.test.tsx +++ b/web/__tests__/apps/app-list-browsing-flow.test.tsx @@ -45,6 +45,7 @@ vi.mock('@/next/navigation', () => ({ push: mockRouterPush, replace: mockRouterReplace, }), + usePathname: () => '/apps', useSearchParams: () => new URLSearchParams(), })) diff --git a/web/__tests__/apps/create-app-flow.test.tsx b/web/__tests__/apps/create-app-flow.test.tsx index a487f102dd..ba3ab166de 100644 --- a/web/__tests__/apps/create-app-flow.test.tsx +++ b/web/__tests__/apps/create-app-flow.test.tsx @@ -42,6 +42,7 @@ vi.mock('@/next/navigation', () => ({ push: mockRouterPush, replace: mockRouterReplace, }), + usePathname: () => '/apps', useSearchParams: () => new URLSearchParams(), })) diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 75442c1ebd..1a8b910bd4 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, screen } from '@testing-library/react' +import { act, fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features' import { renderWithNuqs } from '@/test/nuqs-testing' @@ -13,9 +13,11 @@ const mockUseWorkflowOnlineUsers = vi.hoisted(() => vi.fn((_options: unknown) => const mockReplace = vi.fn() const mockRouter = { replace: mockReplace } +let mockSearchParams = new URLSearchParams('') vi.mock('@/next/navigation', () => ({ useRouter: () => mockRouter, - useSearchParams: () => new URLSearchParams(''), + usePathname: () => '/apps', + useSearchParams: () => mockSearchParams, })) vi.mock('@/service/client', () => ({ @@ -49,12 +51,10 @@ vi.mock('@/context/app-context', () => ({ })) const mockSetKeywords = vi.fn() -const mockSetTagIDs = vi.fn() const mockSetIsCreatedByMe = vi.fn() const mockSetCategory = vi.fn() const mockQueryState = { category: 'all', - tagIDs: [] as string[], keywords: '', isCreatedByMe: false, } @@ -64,11 +64,20 @@ vi.mock('../hooks/use-apps-query-state', () => ({ query: mockQueryState, setCategory: mockSetCategory, setKeywords: mockSetKeywords, - setTagIDs: mockSetTagIDs, setIsCreatedByMe: mockSetIsCreatedByMe, }), })) +vi.mock('@/features/tag-management/components/tag-filter', () => ({ + TagFilter: ({ value, onChange, onOpenTagManagement }: { value: string[], onChange: (value: string[]) => void, onOpenTagManagement: () => void }) => ( +
+ + {value.join(',')} + +
+ ), +})) + let mockOnDSLFileDropped: ((file: File) => void) | null = null let mockDragging = false vi.mock('../hooks/use-dsl-drag-drop', () => ({ @@ -230,6 +239,7 @@ beforeAll(() => { // Render helper wrapping with shared nuqs testing helper plus a seeded // systemFeatures cache so List can resolve its useSuspenseQuery. const renderList = (searchParams = '') => { + mockSearchParams = new URLSearchParams(searchParams) const { wrapper: SystemFeaturesWrapper } = createSystemFeaturesWrapper({ systemFeatures: { branding: { enabled: false } }, }) @@ -253,7 +263,6 @@ describe('List', () => { mockServiceState.isLoading = false mockServiceState.isFetchingNextPage = false mockQueryState.category = 'all' - mockQueryState.tagIDs = [] mockQueryState.keywords = '' mockQueryState.isCreatedByMe = false mockUseWorkflowOnlineUsers.mockClear() @@ -375,12 +384,12 @@ describe('List', () => { describe('App List Query', () => { it('should build paged query input from active filters', () => { - mockQueryState.tagIDs = ['tag-1'] mockQueryState.keywords = 'sales' mockQueryState.isCreatedByMe = true mockQueryState.category = AppModeEnum.WORKFLOW renderList() + fireEvent.click(screen.getByText('common.tag.placeholder')) const options = mockAppListInfiniteOptions.mock.calls.at(-1)?.[0] as AppListInfiniteOptions @@ -397,6 +406,17 @@ describe('List', () => { expect(options.getNextPageParam({ has_more: true, page: 2 })).toBe(3) expect(options.getNextPageParam({ has_more: false, page: 2 })).toBeUndefined() }) + + it('should remove legacy tagIDs from URL while preserving other filters', async () => { + renderList('?category=workflow&tagIDs=tag-1;tag-2&keywords=sales&isCreatedByMe=true') + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith( + '/apps?category=workflow&keywords=sales&isCreatedByMe=true', + { scroll: false }, + ) + }) + }) }) describe('Tag Filter', () => { diff --git a/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx b/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx index 782f6ec353..dfb0c3d7e6 100644 --- a/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx +++ b/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx @@ -5,6 +5,7 @@ import { APP_LIST_SEARCH_DEBOUNCE_MS } from '../../constants' import { useAppsQueryState } from '../use-apps-query-state' const renderWithAdapter = (searchParams = '') => { + // eslint-disable-next-line react/use-state -- renderHook executes a custom hook, not React.useState return renderHookWithNuqs(() => useAppsQueryState(), { searchParams }) } @@ -18,13 +19,11 @@ describe('useAppsQueryState', () => { expect(result.current.query).toEqual({ category: 'all', - tagIDs: [], keywords: '', isCreatedByMe: false, }) expect(typeof result.current.setCategory).toBe('function') expect(typeof result.current.setKeywords).toBe('function') - expect(typeof result.current.setTagIDs).toBe('function') expect(typeof result.current.setIsCreatedByMe).toBe('function') }) @@ -35,7 +34,6 @@ describe('useAppsQueryState', () => { expect(result.current.query).toEqual({ category: AppModeEnum.WORKFLOW, - tagIDs: ['tag1', 'tag2'], keywords: 'search term', isCreatedByMe: true, }) @@ -117,33 +115,6 @@ describe('useAppsQueryState', () => { } }) - it('should update tag filter URL state', async () => { - const { result, onUrlUpdate } = renderWithAdapter() - - act(() => { - result.current.setTagIDs(['tag1', 'tag2']) - }) - - await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls.at(-1)![0] - expect(result.current.query.tagIDs).toEqual(['tag1', 'tag2']) - expect(update.searchParams.get('tagIDs')).toBe('tag1;tag2') - expect(update.options.history).toBe('push') - }) - - it('should remove tagIDs from URL when empty', async () => { - const { result, onUrlUpdate } = renderWithAdapter('?tagIDs=tag1;tag2') - - act(() => { - result.current.setTagIDs([]) - }) - - await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls.at(-1)![0] - expect(result.current.query.tagIDs).toEqual([]) - expect(update.searchParams.has('tagIDs')).toBe(false) - }) - it('should update created-by-me URL state', async () => { const { result, onUrlUpdate } = renderWithAdapter() diff --git a/web/app/components/apps/hooks/use-apps-query-state.ts b/web/app/components/apps/hooks/use-apps-query-state.ts index a0109eb061..d4b4c2fb2d 100644 --- a/web/app/components/apps/hooks/use-apps-query-state.ts +++ b/web/app/components/apps/hooks/use-apps-query-state.ts @@ -1,4 +1,4 @@ -import { debounce, parseAsArrayOf, parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs' +import { debounce, parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs' import { useCallback, useMemo } from 'react' import { AppModes } from '@/types/app' import { APP_LIST_SEARCH_DEBOUNCE_MS } from '../constants' @@ -16,9 +16,6 @@ const appListQueryParsers = { category: parseAsStringLiteral(APP_LIST_CATEGORY_VALUES) .withDefault('all') .withOptions({ history: 'push' }), - tagIDs: parseAsArrayOf(parseAsString, ';') - .withDefault([]) - .withOptions({ history: 'push' }), keywords: parseAsString.withDefault('').withOptions({ limitUrlUpdates: debounce(APP_LIST_SEARCH_DEBOUNCE_MS), }), @@ -38,10 +35,6 @@ export function useAppsQueryState() { setQuery({ keywords }) }, [setQuery]) - const setTagIDs = useCallback((tagIDs: string[]) => { - setQuery({ tagIDs }) - }, [setQuery]) - const setIsCreatedByMe = useCallback((isCreatedByMe: boolean) => { setQuery({ isCreatedByMe }) }, [setQuery]) @@ -50,7 +43,6 @@ export function useAppsQueryState() { query, setCategory, setKeywords, - setTagIDs, setIsCreatedByMe, - }), [query, setCategory, setKeywords, setTagIDs, setIsCreatedByMe]) + }), [query, setCategory, setKeywords, setIsCreatedByMe]) } diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 7eb871c734..c10e1c20e5 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -15,6 +15,7 @@ import { useAppContext } from '@/context/app-context' import { TagFilter } from '@/features/tag-management/components/tag-filter' import { CheckModal } from '@/hooks/use-pay' import dynamic from '@/next/dynamic' +import { usePathname, useRouter, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/client' import { systemFeaturesQueryOptions } from '@/service/system-features' import { AppModeEnum } from '@/types/app' @@ -44,15 +45,18 @@ const List: FC = ({ const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext() + const searchParams = useSearchParams() + const pathname = usePathname() + const { replace } = useRouter() // eslint-disable-next-line react/use-state -- custom URL query hook, not React.useState const { - query: { category, tagIDs, keywords, isCreatedByMe }, + query: { category, keywords, isCreatedByMe }, setCategory, setKeywords, - setTagIDs, setIsCreatedByMe, } = useAppsQueryState() + const [tagIDs, setTagIDs] = useState([]) const debouncedKeywords = useDebounce(keywords, { wait: APP_LIST_SEARCH_DEBOUNCE_MS }) const newAppCardRef = useRef(null) const containerRef = useRef(null) @@ -71,6 +75,16 @@ const List: FC = ({ enabled: isCurrentWorkspaceEditor, }) + useEffect(() => { + if (!searchParams.has('tagIDs')) + return + + const params = new URLSearchParams(searchParams.toString()) + params.delete('tagIDs') + const query = params.toString() + replace(query ? `${pathname}?${query}` : pathname, { scroll: false }) + }, [pathname, replace, searchParams]) + const appListQuery = useMemo(() => ({ page: 1, limit: 30,