refactor(web): migrate document list query state to nuqs (#32339)

This commit is contained in:
yyh
2026-03-03 18:29:23 +08:00
committed by GitHub
parent 3a8ff301fc
commit 1a90c4d81b
49 changed files with 1272 additions and 2003 deletions
@@ -1,21 +1,20 @@
import type { UrlUpdateEvent } from 'nuqs/adapters/testing'
import type { ReactNode } from 'react'
import { act, renderHook } from '@testing-library/react'
import { Provider as JotaiProvider } from 'jotai'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
import { DEFAULT_SORT } from '../constants'
const createWrapper = (searchParams = '') => {
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>()
const { wrapper: NuqsWrapper } = createNuqsTestWrapper({ searchParams })
const wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider>
<NuqsTestingAdapter searchParams={searchParams} onUrlUpdate={onUrlUpdate}>
<NuqsWrapper>
{children}
</NuqsTestingAdapter>
</NuqsWrapper>
</JotaiProvider>
)
return { wrapper, onUrlUpdate }
return { wrapper }
}
describe('Marketplace sort atoms', () => {
@@ -1,9 +1,8 @@
import type { UrlUpdateEvent } from 'nuqs/adapters/testing'
import type { ReactNode } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { Provider as JotaiProvider } from 'jotai'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
import PluginTypeSwitch from '../plugin-type-switch'
vi.mock('#i18n', () => ({
@@ -25,15 +24,15 @@ vi.mock('#i18n', () => ({
}))
const createWrapper = (searchParams = '') => {
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>()
const { wrapper: NuqsWrapper } = createNuqsTestWrapper({ searchParams })
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider>
<NuqsTestingAdapter searchParams={searchParams} onUrlUpdate={onUrlUpdate}>
<NuqsWrapper>
{children}
</NuqsTestingAdapter>
</NuqsWrapper>
</JotaiProvider>
)
return { Wrapper, onUrlUpdate }
return { Wrapper }
}
describe('PluginTypeSwitch', () => {
@@ -2,8 +2,8 @@ import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { Provider as JotaiProvider } from 'jotai'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
vi.mock('@/config', () => ({
API_PREFIX: '/api',
@@ -37,6 +37,7 @@ vi.mock('@/service/client', () => ({
}))
const createWrapper = (searchParams = '') => {
const { wrapper: NuqsWrapper } = createNuqsTestWrapper({ searchParams })
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
@@ -45,9 +46,9 @@ const createWrapper = (searchParams = '') => {
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider>
<QueryClientProvider client={queryClient}>
<NuqsTestingAdapter searchParams={searchParams}>
<NuqsWrapper>
{children}
</NuqsTestingAdapter>
</NuqsWrapper>
</QueryClientProvider>
</JotaiProvider>
)
@@ -1,8 +1,8 @@
import type { ReactNode } from 'react'
import { render } from '@testing-library/react'
import { Provider as JotaiProvider } from 'jotai'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
import StickySearchAndSwitchWrapper from '../sticky-search-and-switch-wrapper'
vi.mock('#i18n', () => ({
@@ -20,13 +20,17 @@ vi.mock('../search-box/search-box-wrapper', () => ({
default: () => <div data-testid="search-box-wrapper">SearchBoxWrapper</div>,
}))
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider>
<NuqsTestingAdapter>
{children}
</NuqsTestingAdapter>
</JotaiProvider>
)
const createWrapper = () => {
const { wrapper: NuqsWrapper } = createNuqsTestWrapper()
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider>
<NuqsWrapper>
{children}
</NuqsWrapper>
</JotaiProvider>
)
return { Wrapper }
}
describe('StickySearchAndSwitchWrapper', () => {
beforeEach(() => {
@@ -34,6 +38,7 @@ describe('StickySearchAndSwitchWrapper', () => {
})
it('should render SearchBoxWrapper and PluginTypeSwitch', () => {
const { Wrapper } = createWrapper()
const { getByTestId } = render(
<StickySearchAndSwitchWrapper />,
{ wrapper: Wrapper },
@@ -44,6 +49,7 @@ describe('StickySearchAndSwitchWrapper', () => {
})
it('should not apply sticky class when no pluginTypeSwitchClassName', () => {
const { Wrapper } = createWrapper()
const { container } = render(
<StickySearchAndSwitchWrapper />,
{ wrapper: Wrapper },
@@ -55,6 +61,7 @@ describe('StickySearchAndSwitchWrapper', () => {
})
it('should apply sticky class when pluginTypeSwitchClassName contains top-', () => {
const { Wrapper } = createWrapper()
const { container } = render(
<StickySearchAndSwitchWrapper pluginTypeSwitchClassName="top-10" />,
{ wrapper: Wrapper },
@@ -67,6 +74,7 @@ describe('StickySearchAndSwitchWrapper', () => {
})
it('should not apply sticky class when pluginTypeSwitchClassName does not contain top-', () => {
const { Wrapper } = createWrapper()
const { container } = render(
<StickySearchAndSwitchWrapper pluginTypeSwitchClassName="custom-class" />,
{ wrapper: Wrapper },
@@ -1,4 +1,5 @@
import type { SearchParams } from 'nuqs/server'
import type { MarketplaceSearchParams } from './search-params'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { createLoader } from 'nuqs/server'
import { getQueryClientServer } from '@/context/query-client-server'
@@ -14,7 +15,7 @@ async function getDehydratedState(searchParams?: Promise<SearchParams>) {
return
}
const loadSearchParams = createLoader(marketplaceSearchParamsParsers)
const params = await loadSearchParams(searchParams)
const params: MarketplaceSearchParams = await loadSearchParams(searchParams)
if (!PLUGIN_CATEGORY_WITH_COLLECTIONS.has(params.category)) {
return
@@ -1,3 +1,4 @@
import type { inferParserType } from 'nuqs/server'
import type { ActivePluginType } from './constants'
import { parseAsArrayOf, parseAsString, parseAsStringEnum } from 'nuqs/server'
import { PLUGIN_TYPE_SEARCH_MAP } from './constants'
@@ -7,3 +8,5 @@ export const marketplaceSearchParamsParsers = {
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
}
export type MarketplaceSearchParams = inferParserType<typeof marketplaceSearchParamsParsers>
@@ -7,6 +7,9 @@ import { PluginPageContext, PluginPageContextProvider, usePluginPageContext } fr
// Mock dependencies
vi.mock('nuqs', () => ({
parseAsStringEnum: vi.fn(() => ({
withDefault: vi.fn(() => ({})),
})),
useQueryState: vi.fn(() => ['plugins', vi.fn()]),
}))
@@ -80,6 +80,9 @@ vi.mock('@/service/use-plugins', () => ({
}))
vi.mock('nuqs', () => ({
parseAsStringEnum: vi.fn(() => ({
withDefault: vi.fn(() => ({})),
})),
useQueryState: vi.fn(() => ['plugins', vi.fn()]),
}))
@@ -3,7 +3,7 @@
import type { ReactNode, RefObject } from 'react'
import type { FilterState } from './filter-management'
import { noop } from 'es-toolkit/function'
import { useQueryState } from 'nuqs'
import { parseAsStringEnum, useQueryState } from 'nuqs'
import {
useMemo,
useRef,
@@ -15,6 +15,19 @@ import {
} from 'use-context-selector'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { PLUGIN_PAGE_TABS_MAP, usePluginPageTabs } from '../hooks'
import { PLUGIN_TYPE_SEARCH_MAP } from '../marketplace/constants'
export type PluginPageTab = typeof PLUGIN_PAGE_TABS_MAP[keyof typeof PLUGIN_PAGE_TABS_MAP]
| (typeof PLUGIN_TYPE_SEARCH_MAP)[keyof typeof PLUGIN_TYPE_SEARCH_MAP]
const PLUGIN_PAGE_TAB_VALUES: PluginPageTab[] = [
PLUGIN_PAGE_TABS_MAP.plugins,
PLUGIN_PAGE_TABS_MAP.marketplace,
...Object.values(PLUGIN_TYPE_SEARCH_MAP),
]
const parseAsPluginPageTab = parseAsStringEnum<PluginPageTab>(PLUGIN_PAGE_TAB_VALUES)
.withDefault(PLUGIN_PAGE_TABS_MAP.plugins)
export type PluginPageContextValue = {
containerRef: RefObject<HTMLDivElement | null>
@@ -22,8 +35,8 @@ export type PluginPageContextValue = {
setCurrentPluginID: (pluginID?: string) => void
filters: FilterState
setFilters: (filter: FilterState) => void
activeTab: string
setActiveTab: (tab: string) => void
activeTab: PluginPageTab
setActiveTab: (tab: PluginPageTab) => void
options: Array<{ value: string, text: string }>
}
@@ -39,7 +52,7 @@ export const PluginPageContext = createContext<PluginPageContextValue>({
searchQuery: '',
},
setFilters: noop,
activeTab: '',
activeTab: PLUGIN_PAGE_TABS_MAP.plugins,
setActiveTab: noop,
options: [],
})
@@ -68,9 +81,7 @@ export const PluginPageContextProvider = ({
const options = useMemo(() => {
return enable_marketplace ? tabs : tabs.filter(tab => tab.value !== PLUGIN_PAGE_TABS_MAP.marketplace)
}, [tabs, enable_marketplace])
const [activeTab, setActiveTab] = useQueryState('tab', {
defaultValue: options[0].value,
})
const [activeTab, setActiveTab] = useQueryState('tab', parseAsPluginPageTab)
return (
<PluginPageContext.Provider
@@ -1,6 +1,7 @@
'use client'
import type { Dependency, PluginDeclaration, PluginManifestInMarket } from '../types'
import type { PluginPageTab } from './context'
import {
RiBookOpenLine,
RiDragDropLine,
@@ -37,6 +38,16 @@ import PluginTasks from './plugin-tasks'
import useReferenceSetting from './use-reference-setting'
import { useUploader } from './use-uploader'
const pluginPageTabSet = new Set<string>([
PLUGIN_PAGE_TABS_MAP.plugins,
PLUGIN_PAGE_TABS_MAP.marketplace,
...Object.values(PLUGIN_TYPE_SEARCH_MAP),
])
const isPluginPageTab = (value: string): value is PluginPageTab => {
return pluginPageTabSet.has(value)
}
export type PluginPageProps = {
plugins: React.ReactNode
marketplace: React.ReactNode
@@ -154,7 +165,10 @@ const PluginPage = ({
<div className="flex-1">
<TabSlider
value={isPluginsTab ? PLUGIN_PAGE_TABS_MAP.plugins : PLUGIN_PAGE_TABS_MAP.marketplace}
onChange={setActiveTab}
onChange={(nextTab) => {
if (isPluginPageTab(nextTab))
setActiveTab(nextTab)
}}
options={options}
/>
</div>