diff --git a/web/app/(commonLayout)/plugins/loading.tsx b/web/app/(commonLayout)/plugins/loading.tsx
new file mode 100644
index 0000000000..f58f96a49c
--- /dev/null
+++ b/web/app/(commonLayout)/plugins/loading.tsx
@@ -0,0 +1,7 @@
+import Loading from '@/app/components/base/loading'
+
+const PluginsLoading = () => {
+ return
+}
+
+export default PluginsLoading
diff --git a/web/app/(commonLayout)/plugins/page.tsx b/web/app/(commonLayout)/plugins/page.tsx
index f366200cf9..c7dc6344e0 100644
--- a/web/app/(commonLayout)/plugins/page.tsx
+++ b/web/app/(commonLayout)/plugins/page.tsx
@@ -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
+}
+
+const PluginList = ({ searchParams }: PluginListProps) => {
return (
}
- marketplace={}
+ marketplace={}
/>
)
}
diff --git a/web/app/components/plugins/marketplace/hydration-server.spec.tsx b/web/app/components/plugins/marketplace/hydration-server.spec.tsx
new file mode 100644
index 0000000000..279c948eea
--- /dev/null
+++ b/web/app/components/plugins/marketplace/hydration-server.spec.tsx
@@ -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
+ }) => options.queryFn({ pageParam: 1, signal: new AbortController().signal })),
+ mockPrefetchQuery: vi.fn(async (options: { queryFn: () => Promise }) => 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: children
,
+ })
+}
+
+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()
+ })
+})
diff --git a/web/app/components/plugins/marketplace/hydration-server.tsx b/web/app/components/plugins/marketplace/hydration-server.tsx
index b01f4dd463..62e4fef626 100644
--- a/web/app/components/plugins/marketplace/hydration-server.tsx
+++ b/web/app/components/plugins/marketplace/hydration-server.tsx
@@ -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) {
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> = []
- 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)
}
diff --git a/web/app/components/plugins/marketplace/query.ts b/web/app/components/plugins/marketplace/query.ts
index 35d99a2bd5..6114f9fd1f 100644
--- a/web/app/components/plugins/marketplace/query.ts
+++ b/web/app/components/plugins/marketplace/query.ts
@@ -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,
})
}
diff --git a/web/app/components/plugins/marketplace/search-params.ts b/web/app/components/plugins/marketplace/search-params.ts
index ad0b16977f..2715deb8a1 100644
--- a/web/app/components/plugins/marketplace/search-params.ts
+++ b/web/app/components/plugins/marketplace/search-params.ts
@@ -3,7 +3,7 @@ import { parseAsArrayOf, parseAsString, parseAsStringEnum } from 'nuqs/server'
import { PLUGIN_TYPE_SEARCH_MAP } from './constants'
export const marketplaceSearchParamsParsers = {
- category: parseAsStringEnum(Object.values(PLUGIN_TYPE_SEARCH_MAP) as ActivePluginType[]).withDefault('all').withOptions({ history: 'replace', clearOnDefault: false }),
+ category: parseAsStringEnum(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' }),
}
diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts
index 4954acd60c..8fef17bba4 100644
--- a/web/app/components/plugins/marketplace/state.ts
+++ b/web/app/components/plugins/marketplace/state.ts
@@ -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,
}
}