From 2d3e244a1f36132e4b561baeb9cbefe269c40388 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:57:43 +0800 Subject: [PATCH] refactor: use path instead of query --- .../components/plugins/marketplace/atoms.ts | 52 +++++++++++++++---- .../plugins/marketplace/description/index.tsx | 2 +- .../plugins/marketplace/hydration-server.tsx | 43 ++------------- .../plugins/marketplace/index.spec.tsx | 4 +- .../plugins/marketplace/list/index.spec.tsx | 6 +-- .../marketplace/list/list-top-info.tsx | 2 +- .../marketplace/list/template-card.tsx | 2 +- .../search-box/search-box-wrapper.tsx | 8 ++- .../search-box/search-dropdown/index.tsx | 2 +- .../plugins/marketplace/search-params.ts | 9 ++-- .../marketplace/sort-dropdown/index.tsx | 2 +- .../components/plugins/marketplace/state.ts | 2 +- .../components/plugins/marketplace/utils.ts | 4 +- .../detail-header/index.tsx | 4 +- .../components/plugins/plugin-item/index.tsx | 2 +- .../plugins/plugin-page/nav-operations.tsx | 6 +-- .../market-place-plugin/action.tsx | 2 +- .../components/switch-plugin-version.tsx | 2 +- 18 files changed, 74 insertions(+), 80 deletions(-) diff --git a/web/app/components/plugins/marketplace/atoms.ts b/web/app/components/plugins/marketplace/atoms.ts index 17bf2fe857..7867660d7e 100644 --- a/web/app/components/plugins/marketplace/atoms.ts +++ b/web/app/components/plugins/marketplace/atoms.ts @@ -1,6 +1,7 @@ import type { SearchTab } from './search-params' import type { PluginsSort, SearchParamsFromCollection } from './types' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' +import { useParams, usePathname, useRouter } from 'next/navigation' import { useQueryState } from 'nuqs' import { useCallback, useMemo } from 'react' import { CATEGORY_ALL, DEFAULT_PLUGIN_SORT, DEFAULT_TEMPLATE_SORT, getValidatedPluginCategory, getValidatedTemplateCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants' @@ -32,24 +33,57 @@ export function useSearchText() { return useQueryState('q', marketplaceSearchParamsParsers.q) } export function useActivePluginCategory() { - const [category, setCategory] = useQueryState('category', marketplaceSearchParamsParsers.category) - return [getValidatedPluginCategory(category), setCategory] as const + const router = useRouter() + const pathname = usePathname() + const segments = pathname.split('/').filter(Boolean) + const categoryFromPath = segments[1] || CATEGORY_ALL + const validatedCategory = getValidatedPluginCategory(categoryFromPath) + const handleChange = (newCategory: string) => { + const newPathSegments = [...segments] + newPathSegments[1] = newCategory + const newPath = `/${newPathSegments.join('/')}` + router.push(newPath) + } + return [validatedCategory, handleChange] as const } export function useActiveTemplateCategory() { - const [category, setCategory] = useQueryState('category', marketplaceSearchParamsParsers.category) - return [getValidatedTemplateCategory(category), setCategory] as const + const router = useRouter() + const pathname = usePathname() + const segments = pathname.split('/').filter(Boolean) + const categoryFromPath = segments[1] || CATEGORY_ALL + const validatedCategory = getValidatedTemplateCategory(categoryFromPath) + const handleChange = (newCategory: string) => { + router.push(`/${CREATION_TYPE.templates}/${newCategory}`) + } + return [validatedCategory, handleChange] as const } export function useFilterPluginTags() { return useQueryState('tags', marketplaceSearchParamsParsers.tags) } export function useSearchTab() { - return useQueryState('searchTab', marketplaceSearchParamsParsers.searchTab) + const router = useRouter() + // /search/[searchTab] + const { searchTab } = useParams() + const handleChange = useCallback( + (newTab: string) => { + const location = new URL(window.location.href) + location.pathname = `/search/${newTab}` + router.push(location.href) + }, + [router], + ) + return [searchTab, handleChange] as const } export function useCreationType() { - return useQueryState('creationType', marketplaceSearchParamsParsers.creationType) + const pathname = usePathname() + const segments = pathname.split('/').filter(Boolean) + + if (segments[0] === CREATION_TYPE.templates) + return CREATION_TYPE.templates + return CREATION_TYPE.plugins } // Search-page-specific filter hooks (separate from list-page category/tags) @@ -77,7 +111,7 @@ export function useSearchFilterTags() { export const searchModeAtom = atom(null) export function useMarketplaceSearchMode() { - const [creationType] = useCreationType() + const creationType = useCreationType() const [searchText] = useSearchText() const [searchTab] = useSearchTab() const [filterPluginTags] = useFilterPluginTags() @@ -98,7 +132,7 @@ export function useMarketplaceSearchMode() { * Plugins use `marketplacePluginSortAtom`, templates use `marketplaceTemplateSortAtom`. */ export function useActiveSort(): [PluginsSort, (sort: PluginsSort) => void] { - const [creationType] = useCreationType() + const creationType = useCreationType() const [pluginSort, setPluginSort] = useAtom(marketplacePluginSortAtom) const [templateSort, setTemplateSort] = useAtom(marketplaceTemplateSortAtom) const isTemplates = creationType === CREATION_TYPE.templates @@ -112,7 +146,7 @@ export function useActiveSort(): [PluginsSort, (sort: PluginsSort) => void] { } export function useActiveSortValue(): PluginsSort { - const [creationType] = useCreationType() + const creationType = useCreationType() const pluginSort = useAtomValue(marketplacePluginSortAtom) const templateSort = useAtomValue(marketplaceTemplateSortAtom) return creationType === CREATION_TYPE.templates ? templateSort : pluginSort diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index 9d5a07cd48..ecfda7aa6c 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -30,7 +30,7 @@ export const Description = ({ marketplaceNav, }: DescriptionProps) => { const { t } = useTranslation('plugin') - const [creationType] = useCreationType() + const creationType = useCreationType() const isTemplatesView = creationType === CREATION_TYPE.templates const heroTitleKey = isTemplatesView ? 'marketplace.templatesHeroTitle' : 'marketplace.pluginsHeroTitle' const heroSubtitleKey = isTemplatesView ? 'marketplace.templatesHeroSubtitle' : 'marketplace.pluginsHeroSubtitle' diff --git a/web/app/components/plugins/marketplace/hydration-server.tsx b/web/app/components/plugins/marketplace/hydration-server.tsx index 881c825f4a..6b67e554c0 100644 --- a/web/app/components/plugins/marketplace/hydration-server.tsx +++ b/web/app/components/plugins/marketplace/hydration-server.tsx @@ -1,54 +1,19 @@ import type { SearchParams } from 'nuqs' -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 { getValidatedPluginCategory, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants' -import { CREATION_TYPE, marketplaceSearchParamsParsers } from './search-params' -import { getCollectionsParams, getMarketplaceCollectionsAndPlugins, getMarketplaceTemplateCollectionsAndTemplates } from './utils' +import { HydrationBoundary } from '@tanstack/react-query' // The server side logic should move to marketplace's codebase so that we can get rid of Next.js -async function getDehydratedState(searchParams?: Promise) { - if (!searchParams) { - return - } - const loadSearchParams = createLoader(marketplaceSearchParamsParsers) - const params = await loadSearchParams(searchParams) - const queryClient = getQueryClientServer() - - if (params.creationType === CREATION_TYPE.templates) { - await queryClient.prefetchQuery({ - queryKey: marketplaceQuery.templateCollections.list.queryKey({ input: { query: undefined } }), - queryFn: () => getMarketplaceTemplateCollectionsAndTemplates(), - }) - return dehydrate(queryClient) - } - - const pluginCategory = getValidatedPluginCategory(params.category) - - if (!PLUGIN_CATEGORY_WITH_COLLECTIONS.has(pluginCategory)) { - return - } - - const collectionsParams = getCollectionsParams(pluginCategory) - await queryClient.prefetchQuery({ - queryKey: marketplaceQuery.plugins.collections.queryKey({ input: { query: collectionsParams } }), - queryFn: () => getMarketplaceCollectionsAndPlugins(collectionsParams), - }) - return dehydrate(queryClient) -} - export async function HydrateQueryClient({ + // eslint-disable-next-line unused-imports/no-unused-vars searchParams, children, }: { searchParams: Promise | undefined children: React.ReactNode }) { - const dehydratedState = await getDehydratedState(searchParams) + // TODO: bring back dehydrated state return ( - + {children} ) diff --git a/web/app/components/plugins/marketplace/index.spec.tsx b/web/app/components/plugins/marketplace/index.spec.tsx index 954d50e9de..129ab0dc69 100644 --- a/web/app/components/plugins/marketplace/index.spec.tsx +++ b/web/app/components/plugins/marketplace/index.spec.tsx @@ -530,7 +530,7 @@ describe('utils', () => { const plugin = createMockPlugin({ org: 'test-org', name: 'test-plugin', type: 'plugin' }) const link = getPluginLinkInMarketplace(plugin) - expect(link).toBe('https://marketplace.dify.ai/plugins/test-org/test-plugin') + expect(link).toBe('https://marketplace.dify.ai/plugin/test-org/test-plugin') }) it('should return correct link for bundle', () => { @@ -546,7 +546,7 @@ describe('utils', () => { const plugin = createMockPlugin({ org: 'test-org', name: 'test-plugin', type: 'plugin' }) const link = getPluginDetailLinkInMarketplace(plugin) - expect(link).toBe('/plugins/test-org/test-plugin') + expect(link).toBe('/plugin/test-org/test-plugin') }) it('should return correct detail link for bundle', () => { diff --git a/web/app/components/plugins/marketplace/list/index.spec.tsx b/web/app/components/plugins/marketplace/list/index.spec.tsx index 05f74d93d7..e6f2b0ff48 100644 --- a/web/app/components/plugins/marketplace/list/index.spec.tsx +++ b/web/app/components/plugins/marketplace/list/index.spec.tsx @@ -120,9 +120,9 @@ vi.mock('../utils', async (importOriginal) => { return { ...actual, getPluginLinkInMarketplace: (plugin: Plugin, _params?: Record) => - `/plugins/${plugin.org}/${plugin.name}`, + `/plugin/${plugin.org}/${plugin.name}`, getPluginDetailLinkInMarketplace: (plugin: Plugin) => - `/plugins/${plugin.org}/${plugin.name}`, + `/plugin/${plugin.org}/${plugin.name}`, } }) @@ -1166,7 +1166,7 @@ describe('CardWrapper (via List integration)', () => { ) const detailLink = screen.getByText('Detail').closest('a') - expect(detailLink).toHaveAttribute('href', '/plugins/test-org/link-test-plugin') + expect(detailLink).toHaveAttribute('href', '/plugin/test-org/link-test-plugin') expect(detailLink).toHaveAttribute('target', '_blank') }) diff --git a/web/app/components/plugins/marketplace/list/list-top-info.tsx b/web/app/components/plugins/marketplace/list/list-top-info.tsx index 4dfee39c34..a9132c1bb6 100644 --- a/web/app/components/plugins/marketplace/list/list-top-info.tsx +++ b/web/app/components/plugins/marketplace/list/list-top-info.tsx @@ -15,7 +15,7 @@ import { CREATION_TYPE } from '../search-params' import SortDropdown from '../sort-dropdown' const ListTopInfo = () => { - const [creationType] = useCreationType() + const creationType = useCreationType() const { t } = useTranslation() const [filterPluginTags] = useFilterPluginTags() const [activePluginCategory] = useActivePluginCategory() diff --git a/web/app/components/plugins/marketplace/list/template-card.tsx b/web/app/components/plugins/marketplace/list/template-card.tsx index 64b48f4607..c9d8f3fa00 100644 --- a/web/app/components/plugins/marketplace/list/template-card.tsx +++ b/web/app/components/plugins/marketplace/list/template-card.tsx @@ -34,7 +34,7 @@ const TemplateCardComponent = ({ const iconUrl = getTemplateIconUrl(template) const handleClick = useCallback(() => { - const url = getMarketplaceUrl(`/templates/${publisher_handle}/${template_name}`, { + const url = getMarketplaceUrl(`/template/${publisher_handle}/${template_name}`, { theme, language: locale, templateId: id, diff --git a/web/app/components/plugins/marketplace/search-box/search-box-wrapper.tsx b/web/app/components/plugins/marketplace/search-box/search-box-wrapper.tsx index 1d7e3fd924..a6bffaafe4 100644 --- a/web/app/components/plugins/marketplace/search-box/search-box-wrapper.tsx +++ b/web/app/components/plugins/marketplace/search-box/search-box-wrapper.tsx @@ -4,6 +4,7 @@ import type { UnifiedSearchParams } from '../types' import { useTranslation } from '#i18n' import { useDebounce } from 'ahooks' import { useSetAtom } from 'jotai' +import { useRouter } from 'next/navigation' import { useMemo, useState } from 'react' import Input from '@/app/components/base/input' import { @@ -14,7 +15,6 @@ import { import { cn } from '@/utils/classnames' import { searchModeAtom, - useSearchTab, useSearchText, } from '../atoms' import { useMarketplaceUnifiedSearch } from '../query' @@ -31,7 +31,6 @@ const SearchBoxWrapper = ({ }: SearchBoxWrapperProps) => { const { t } = useTranslation() const [searchText, handleSearchTextChange] = useSearchText() - const [, setSearchTab] = useSearchTab() const setSearchMode = useSetAtom(searchModeAtom) const committedSearch = searchText || '' const [draftSearch, setDraftSearch] = useState(committedSearch) @@ -39,6 +38,7 @@ const SearchBoxWrapper = ({ const [isHoveringDropdown, setIsHoveringDropdown] = useState(false) const debouncedDraft = useDebounce(draftSearch, { wait: 300 }) const hasDraft = !!debouncedDraft.trim() + const router = useRouter() const dropdownQueryParams = useMemo((): UnifiedSearchParams | undefined => { if (!hasDraft) @@ -68,9 +68,7 @@ const SearchBoxWrapper = ({ const trimmed = draftSearch.trim() if (!trimmed) return - handleSearchTextChange(trimmed) - setSearchTab('all') - setSearchMode(true) + router.push(`/search/all/?q=${encodeURIComponent(trimmed)}`) setIsFocused(false) } diff --git a/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx b/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx index b05c6eef43..6eef0b013b 100644 --- a/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx +++ b/web/app/components/plugins/marketplace/search-box/search-dropdown/index.tsx @@ -180,7 +180,7 @@ function TemplatesSection({ templates, t }: { return ( ([CREATION_TYPE.plugins, CREATION_TYPE.templates]).withDefault(CREATION_TYPE.plugins).withOptions({ history: 'replace' }), - searchTab: parseAsStringEnum(['all', 'plugins', 'templates', 'creators']).withDefault('').withOptions({ history: 'replace' }), // Search-page-specific filters (independent from list-page category/tags) searchCategories: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }), searchLanguages: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }), searchType: parseAsString.withDefault('all').withOptions({ history: 'replace' }), searchTags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }), } - -export type SearchTab = 'all' | 'plugins' | 'templates' | 'creators' | '' diff --git a/web/app/components/plugins/marketplace/sort-dropdown/index.tsx b/web/app/components/plugins/marketplace/sort-dropdown/index.tsx index f710546a6e..77b5f0b180 100644 --- a/web/app/components/plugins/marketplace/sort-dropdown/index.tsx +++ b/web/app/components/plugins/marketplace/sort-dropdown/index.tsx @@ -29,7 +29,7 @@ const TEMPLATE_SORT_OPTIONS = [ const SortDropdown = () => { const { t } = useTranslation() - const [creationType] = useCreationType() + const creationType = useCreationType() const isTemplates = creationType === CREATION_TYPE.templates const rawOptions = isTemplates ? TEMPLATE_SORT_OPTIONS : PLUGIN_SORT_OPTIONS diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts index 96b412ff80..52de55373e 100644 --- a/web/app/components/plugins/marketplace/state.ts +++ b/web/app/components/plugins/marketplace/state.ts @@ -134,7 +134,7 @@ export function isPluginsData(data: MarketplaceData): data is PluginsMarketplace * Returns either plugins or templates data based on URL parameter */ export function useMarketplaceData(): MarketplaceData { - const [creationType] = useCreationType() + const creationType = useCreationType() const pluginsData = usePluginsMarketplaceData(creationType === CREATION_TYPE.plugins) const templatesData = useTemplatesMarketplaceData(creationType === CREATION_TYPE.templates) diff --git a/web/app/components/plugins/marketplace/utils.ts b/web/app/components/plugins/marketplace/utils.ts index 19d9d591e6..7f78240588 100644 --- a/web/app/components/plugins/marketplace/utils.ts +++ b/web/app/components/plugins/marketplace/utils.ts @@ -89,13 +89,13 @@ export const getFormattedPlugin = (bundle: Plugin): Plugin => { export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record) => { if (plugin.type === 'bundle') return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params) - return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params) + return getMarketplaceUrl(`/plugin/${plugin.org}/${plugin.name}`, params) } export const getPluginDetailLinkInMarketplace = (plugin: Plugin) => { if (plugin.type === 'bundle') return `/bundles/${plugin.org}/${plugin.name}` - return `/plugins/${plugin.org}/${plugin.name}` + return `/plugin/${plugin.org}/${plugin.name}` } export const getMarketplacePluginsByCollectionId = async ( diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx index 8f265c5717..fd7b88ff85 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx @@ -65,7 +65,7 @@ const getDetailUrl = ( return `https://github.com/${repo}` } if (source === PluginSource.marketplace) - return getMarketplaceUrl(`/plugins/${author}/${name}`, { language: locale, theme }) + return getMarketplaceUrl(`/plugin/${author}/${name}`, { language: locale, theme }) return '' } @@ -249,7 +249,7 @@ const DetailHeader = ({ status={status} deprecatedReason={deprecated_reason} alternativePluginId={alternative_plugin_id} - alternativePluginURL={getMarketplaceUrl(`/plugins/${alternative_plugin_id}`, { language: currentLocale, theme })} + alternativePluginURL={getMarketplaceUrl(`/plugin/${alternative_plugin_id}`, { language: currentLocale, theme })} className="mt-3" /> )} diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 3f658c63a8..9e660e67f0 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -195,7 +195,7 @@ const PluginItem: FC = ({ {source === PluginSource.marketplace && enable_marketplace && ( <> - +
{t('from', { ns: 'plugin' })} {' '} diff --git a/web/app/components/plugins/plugin-page/nav-operations.tsx b/web/app/components/plugins/plugin-page/nav-operations.tsx index c739922636..e26e8c51ef 100644 --- a/web/app/components/plugins/plugin-page/nav-operations.tsx +++ b/web/app/components/plugins/plugin-page/nav-operations.tsx @@ -105,12 +105,12 @@ export const SubmitRequestDropdown = () => { export const CreationTypeTabs = () => { const { t } = useTranslation() - const [creationType] = useCreationType() + const creationType = useCreationType() return (
{ = ({ diff --git a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx index eb32c0595f..c3569d1f78 100644 --- a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -93,7 +93,7 @@ export const SwitchPluginVersion: FC = (props) => { modalBottomLeft={(