mirror of
https://github.com/langgenius/dify.git
synced 2026-10-28 03:35:04 +08:00
refactor(web): migrate document list query state to nuqs (#32339)
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
+16
-8
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user