mirror of
https://github.com/langgenius/dify.git
synced 2026-02-20 07:45:10 +00:00
Compare commits
1 Commits
refactor/r
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2546caa1f |
7
web/app/(commonLayout)/plugins/loading.tsx
Normal file
7
web/app/(commonLayout)/plugins/loading.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
const PluginsLoading = () => {
|
||||
return <Loading type="app" />
|
||||
}
|
||||
|
||||
export default PluginsLoading
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { SearchParams } from 'nuqs'
|
||||
import Marketplace from '@/app/components/plugins/marketplace'
|
||||
import PluginPage from '@/app/components/plugins/plugin-page'
|
||||
import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel'
|
||||
|
||||
const PluginList = () => {
|
||||
type PluginListProps = {
|
||||
searchParams: Promise<SearchParams>
|
||||
}
|
||||
|
||||
const PluginList = ({ searchParams }: PluginListProps) => {
|
||||
return (
|
||||
<PluginPage
|
||||
plugins={<PluginsPanel />}
|
||||
marketplace={<Marketplace pluginTypeSwitchClassName="top-[60px]" />}
|
||||
marketplace={<Marketplace pluginTypeSwitchClassName="top-[60px]" searchParams={searchParams} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
134
web/app/components/plugins/marketplace/hydration-server.spec.tsx
Normal file
134
web/app/components/plugins/marketplace/hydration-server.spec.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import type { SearchParams } from 'nuqs'
|
||||
import type { ReactNode } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockDehydrate,
|
||||
mockGetCollectionsParams,
|
||||
mockGetMarketplaceCollectionsAndPlugins,
|
||||
mockGetMarketplaceListFilterType,
|
||||
mockGetMarketplacePlugins,
|
||||
mockPrefetchInfiniteQuery,
|
||||
mockPrefetchQuery,
|
||||
} = vi.hoisted(() => ({
|
||||
mockDehydrate: vi.fn(() => ({ dehydrated: true })),
|
||||
mockGetCollectionsParams: vi.fn((category: string) => ({ category })),
|
||||
mockGetMarketplaceCollectionsAndPlugins: vi.fn(async () => ({ marketplaceCollections: [], marketplaceCollectionPluginsMap: {} })),
|
||||
mockGetMarketplaceListFilterType: vi.fn((category: string) => (category === 'bundle' ? 'bundle' : undefined)),
|
||||
mockGetMarketplacePlugins: vi.fn(async () => ({ plugins: [], total: 0, page: 1, page_size: 40 })),
|
||||
mockPrefetchInfiniteQuery: vi.fn(async (options: {
|
||||
queryFn: (context: { pageParam: number, signal: AbortSignal }) => Promise<unknown>
|
||||
}) => options.queryFn({ pageParam: 1, signal: new AbortController().signal })),
|
||||
mockPrefetchQuery: vi.fn(async (options: { queryFn: () => Promise<unknown> }) => options.queryFn()),
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
dehydrate: mockDehydrate,
|
||||
HydrationBoundary: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/query-client-server', () => ({
|
||||
getQueryClientServer: () => ({
|
||||
prefetchQuery: mockPrefetchQuery,
|
||||
prefetchInfiniteQuery: mockPrefetchInfiniteQuery,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceQuery: {
|
||||
collections: {
|
||||
queryKey: vi.fn((input: unknown) => ['collections', input]),
|
||||
},
|
||||
searchAdvanced: {
|
||||
queryKey: vi.fn((input: unknown) => ['searchAdvanced', input]),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./utils', () => ({
|
||||
getCollectionsParams: mockGetCollectionsParams,
|
||||
getMarketplaceCollectionsAndPlugins: mockGetMarketplaceCollectionsAndPlugins,
|
||||
getMarketplaceListFilterType: mockGetMarketplaceListFilterType,
|
||||
getMarketplacePlugins: mockGetMarketplacePlugins,
|
||||
}))
|
||||
|
||||
const renderHydration = async (searchParams?: SearchParams) => {
|
||||
const { HydrateQueryClient } = await import('./hydration-server')
|
||||
return HydrateQueryClient({
|
||||
searchParams: searchParams ? Promise.resolve(searchParams) : undefined,
|
||||
children: <div>children</div>,
|
||||
})
|
||||
}
|
||||
|
||||
describe('HydrateQueryClient', () => {
|
||||
beforeEach(() => {
|
||||
mockDehydrate.mockClear()
|
||||
mockGetCollectionsParams.mockClear()
|
||||
mockGetMarketplaceCollectionsAndPlugins.mockClear()
|
||||
mockGetMarketplaceListFilterType.mockClear()
|
||||
mockGetMarketplacePlugins.mockClear()
|
||||
mockPrefetchInfiniteQuery.mockClear()
|
||||
mockPrefetchQuery.mockClear()
|
||||
})
|
||||
|
||||
it('should prefetch collections query for default non-search mode', async () => {
|
||||
await renderHydration({ category: 'all' })
|
||||
|
||||
expect(mockPrefetchQuery).toHaveBeenCalledTimes(1)
|
||||
expect(mockPrefetchInfiniteQuery).not.toHaveBeenCalled()
|
||||
expect(mockGetCollectionsParams).toHaveBeenCalledWith('all')
|
||||
expect(mockGetMarketplaceCollectionsAndPlugins).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetMarketplacePlugins).not.toHaveBeenCalled()
|
||||
expect(mockDehydrate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should prefetch searchAdvanced query when query text exists', async () => {
|
||||
await renderHydration({ category: 'all', q: 'search-term' })
|
||||
|
||||
expect(mockPrefetchQuery).not.toHaveBeenCalled()
|
||||
expect(mockPrefetchInfiniteQuery).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetMarketplaceCollectionsAndPlugins).not.toHaveBeenCalled()
|
||||
expect(mockGetMarketplacePlugins).toHaveBeenCalledWith(
|
||||
{
|
||||
query: 'search-term',
|
||||
category: undefined,
|
||||
tags: [],
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
type: undefined,
|
||||
},
|
||||
1,
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
expect(mockDehydrate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should prefetch searchAdvanced query for non-collection category', async () => {
|
||||
await renderHydration({ category: 'model' })
|
||||
|
||||
expect(mockPrefetchQuery).not.toHaveBeenCalled()
|
||||
expect(mockPrefetchInfiniteQuery).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetMarketplaceCollectionsAndPlugins).not.toHaveBeenCalled()
|
||||
expect(mockGetMarketplaceListFilterType).toHaveBeenCalledWith('model')
|
||||
expect(mockGetMarketplacePlugins).toHaveBeenCalledWith(
|
||||
{
|
||||
query: '',
|
||||
category: 'model',
|
||||
tags: [],
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
type: undefined,
|
||||
},
|
||||
1,
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
})
|
||||
|
||||
it('should skip prefetch when search params are missing', async () => {
|
||||
await renderHydration()
|
||||
|
||||
expect(mockPrefetchQuery).not.toHaveBeenCalled()
|
||||
expect(mockPrefetchInfiniteQuery).not.toHaveBeenCalled()
|
||||
expect(mockDehydrate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { SearchParams } from 'nuqs'
|
||||
import type { PluginsSearchParams } from './types'
|
||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
|
||||
import { createLoader } from 'nuqs/server'
|
||||
import { getQueryClientServer } from '@/context/query-client-server'
|
||||
import { marketplaceQuery } from '@/service/client'
|
||||
import { PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
|
||||
import { DEFAULT_SORT, PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from './constants'
|
||||
import { marketplaceSearchParamsParsers } from './search-params'
|
||||
import { getCollectionsParams, getMarketplaceCollectionsAndPlugins } from './utils'
|
||||
import {
|
||||
getCollectionsParams,
|
||||
getMarketplaceCollectionsAndPlugins,
|
||||
getMarketplaceListFilterType,
|
||||
getMarketplacePlugins,
|
||||
} from './utils'
|
||||
|
||||
// The server side logic should move to marketplace's codebase so that we can get rid of Next.js
|
||||
|
||||
@@ -13,19 +19,49 @@ async function getDehydratedState(searchParams?: Promise<SearchParams>) {
|
||||
if (!searchParams) {
|
||||
return
|
||||
}
|
||||
|
||||
const loadSearchParams = createLoader(marketplaceSearchParamsParsers)
|
||||
const params = await loadSearchParams(searchParams)
|
||||
const queryClient = getQueryClientServer()
|
||||
const isSearchMode = !!params.q
|
||||
|| params.tags.length > 0
|
||||
|| !PLUGIN_CATEGORY_WITH_COLLECTIONS.has(params.category)
|
||||
const prefetchTasks: Array<Promise<unknown>> = []
|
||||
|
||||
if (!PLUGIN_CATEGORY_WITH_COLLECTIONS.has(params.category)) {
|
||||
if (!isSearchMode && PLUGIN_CATEGORY_WITH_COLLECTIONS.has(params.category)) {
|
||||
prefetchTasks.push(queryClient.prefetchQuery({
|
||||
queryKey: marketplaceQuery.collections.queryKey({ input: { query: getCollectionsParams(params.category) } }),
|
||||
queryFn: () => getMarketplaceCollectionsAndPlugins(getCollectionsParams(params.category)),
|
||||
}))
|
||||
}
|
||||
|
||||
if (isSearchMode) {
|
||||
const queryParams: PluginsSearchParams = {
|
||||
query: params.q,
|
||||
category: params.category === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : params.category,
|
||||
tags: params.tags,
|
||||
sort_by: DEFAULT_SORT.sortBy,
|
||||
sort_order: DEFAULT_SORT.sortOrder,
|
||||
type: getMarketplaceListFilterType(params.category),
|
||||
}
|
||||
|
||||
prefetchTasks.push(queryClient.prefetchInfiniteQuery({
|
||||
queryKey: marketplaceQuery.searchAdvanced.queryKey({
|
||||
input: {
|
||||
body: queryParams,
|
||||
params: { kind: queryParams.type === 'bundle' ? 'bundles' : 'plugins' },
|
||||
},
|
||||
}),
|
||||
queryFn: ({ pageParam = 1, signal }) => getMarketplacePlugins(queryParams, Number(pageParam), signal),
|
||||
initialPageParam: 1,
|
||||
}))
|
||||
}
|
||||
|
||||
if (!prefetchTasks.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const queryClient = getQueryClientServer()
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: marketplaceQuery.collections.queryKey({ input: { query: getCollectionsParams(params.category) } }),
|
||||
queryFn: () => getMarketplaceCollectionsAndPlugins(getCollectionsParams(params.category)),
|
||||
})
|
||||
await Promise.all(prefetchTasks)
|
||||
return dehydrate(queryClient)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,18 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { marketplaceQuery } from '@/service/client'
|
||||
import { getMarketplaceCollectionsAndPlugins, getMarketplacePlugins } from './utils'
|
||||
|
||||
type CollectionsQueryOptions = {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function useMarketplaceCollectionsAndPlugins(
|
||||
collectionsParams: MarketPlaceInputs['collections']['query'],
|
||||
options?: CollectionsQueryOptions,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: marketplaceQuery.collections.queryKey({ input: { query: collectionsParams } }),
|
||||
queryFn: ({ signal }) => getMarketplaceCollectionsAndPlugins(collectionsParams, { signal }),
|
||||
enabled: options?.enabled ?? true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { parseAsArrayOf, parseAsString, parseAsStringEnum } from 'nuqs/server'
|
||||
import { PLUGIN_TYPE_SEARCH_MAP } from './constants'
|
||||
|
||||
export const marketplaceSearchParamsParsers = {
|
||||
category: parseAsStringEnum<ActivePluginType>(Object.values(PLUGIN_TYPE_SEARCH_MAP) as ActivePluginType[]).withDefault('all').withOptions({ history: 'replace', clearOnDefault: false }),
|
||||
category: parseAsStringEnum<ActivePluginType>(Object.values(PLUGIN_TYPE_SEARCH_MAP) as ActivePluginType[]).withDefault('all').withOptions({ history: 'replace' }),
|
||||
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
|
||||
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PluginsSearchParams } from './types'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useActivePluginType, useFilterPluginTags, useMarketplaceSearchMode, useMarketplaceSortValue, useSearchPluginText } from './atoms'
|
||||
import { PLUGIN_TYPE_SEARCH_MAP } from './constants'
|
||||
import { PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from './constants'
|
||||
import { useMarketplaceContainerScroll } from './hooks'
|
||||
import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins } from './query'
|
||||
import { getCollectionsParams, getMarketplaceListFilterType } from './utils'
|
||||
@@ -12,13 +12,16 @@ export function useMarketplaceData() {
|
||||
const searchPluginText = useDebounce(searchPluginTextOriginal, { wait: 500 })
|
||||
const [filterPluginTags] = useFilterPluginTags()
|
||||
const [activePluginType] = useActivePluginType()
|
||||
const isSearchMode = useMarketplaceSearchMode()
|
||||
|
||||
const collectionsQuery = useMarketplaceCollectionsAndPlugins(
|
||||
getCollectionsParams(activePluginType),
|
||||
{
|
||||
enabled: !isSearchMode && PLUGIN_CATEGORY_WITH_COLLECTIONS.has(activePluginType),
|
||||
},
|
||||
)
|
||||
|
||||
const sort = useMarketplaceSortValue()
|
||||
const isSearchMode = useMarketplaceSearchMode()
|
||||
const queryParams = useMemo((): PluginsSearchParams | undefined => {
|
||||
if (!isSearchMode)
|
||||
return undefined
|
||||
@@ -49,7 +52,7 @@ export function useMarketplaceData() {
|
||||
plugins: pluginsQuery.data?.pages.flatMap(page => page.plugins),
|
||||
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
|
||||
page: pluginsQuery.data?.pages.length || 1,
|
||||
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
|
||||
isLoading: (isSearchMode ? false : collectionsQuery.isLoading) || pluginsQuery.isLoading,
|
||||
isFetchingNextPage,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user