mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 14:14:17 +08:00
refactor: use path instead of query
This commit is contained in:
parent
0db446b8ea
commit
2d3e244a1f
@ -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<true | null>(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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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<SearchParams>) {
|
||||
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<SearchParams> | undefined
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const dehydratedState = await getDehydratedState(searchParams)
|
||||
// TODO: bring back dehydrated state
|
||||
return (
|
||||
<HydrationBoundary state={dehydratedState}>
|
||||
<HydrationBoundary state={null}>
|
||||
{children}
|
||||
</HydrationBoundary>
|
||||
)
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -120,9 +120,9 @@ vi.mock('../utils', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
getPluginLinkInMarketplace: (plugin: Plugin, _params?: Record<string, string | undefined>) =>
|
||||
`/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')
|
||||
})
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -180,7 +180,7 @@ function TemplatesSection({ templates, t }: {
|
||||
return (
|
||||
<DropdownItem
|
||||
key={template.id}
|
||||
href={getMarketplaceUrl(`/templates/${template.publisher_handle}/${template.template_name}`, { templateId: template.id })}
|
||||
href={getMarketplaceUrl(`/template/${template.publisher_handle}/${template.template_name}`, { templateId: template.id })}
|
||||
icon={(
|
||||
<div className="flex shrink-0 items-start py-1">
|
||||
<AppIcon
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { parseAsArrayOf, parseAsString, parseAsStringEnum } from 'nuqs/server'
|
||||
import { parseAsArrayOf, parseAsString } from 'nuqs/server'
|
||||
|
||||
export const CREATION_TYPE = {
|
||||
plugins: 'plugins',
|
||||
@ -6,18 +6,15 @@ export const CREATION_TYPE = {
|
||||
} as const
|
||||
|
||||
export type CreationType = typeof CREATION_TYPE[keyof typeof CREATION_TYPE]
|
||||
export const SEARCH_TABS = ['all', 'plugins', 'templates', 'creators'] as const
|
||||
export type SearchTab = (typeof SEARCH_TABS)[number] | ''
|
||||
|
||||
export const marketplaceSearchParamsParsers = {
|
||||
category: parseAsString.withDefault('all').withOptions({ history: 'replace', clearOnDefault: false }),
|
||||
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
|
||||
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
||||
creationType: parseAsStringEnum<CreationType>([CREATION_TYPE.plugins, CREATION_TYPE.templates]).withDefault(CREATION_TYPE.plugins).withOptions({ history: 'replace' }),
|
||||
searchTab: parseAsStringEnum<SearchTab>(['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' | ''
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -89,13 +89,13 @@ export const getFormattedPlugin = (bundle: Plugin): Plugin => {
|
||||
export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record<string, string | undefined>) => {
|
||||
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 (
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -195,7 +195,7 @@ const PluginItem: FC<Props> = ({
|
||||
{source === PluginSource.marketplace && enable_marketplace
|
||||
&& (
|
||||
<>
|
||||
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target="_blank" className="flex items-center gap-0.5">
|
||||
<a href={getMarketplaceUrl(`/plugin/${author}/${name}`, { theme })} target="_blank" className="flex items-center gap-0.5">
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t('from', { ns: 'plugin' })}
|
||||
{' '}
|
||||
|
||||
@ -105,12 +105,12 @@ export const SubmitRequestDropdown = () => {
|
||||
|
||||
export const CreationTypeTabs = () => {
|
||||
const { t } = useTranslation()
|
||||
const [creationType] = useCreationType()
|
||||
const creationType = useCreationType()
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Link
|
||||
href={`/?creationType=${CREATION_TYPE.plugins}`}
|
||||
href={`/${CREATION_TYPE.plugins}`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'flex items-center gap-1 px-3 py-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
@ -123,7 +123,7 @@ export const CreationTypeTabs = () => {
|
||||
</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/?creationType=${CREATION_TYPE.templates}`}
|
||||
href={`/${CREATION_TYPE.templates}`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'flex items-center gap-1 px-3 py-2 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
|
||||
@ -92,7 +92,7 @@ const OperationDropdown: FC<Props> = ({
|
||||
<PortalToFollowElemContent className="z-[9999]">
|
||||
<div className="min-w-[176px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
||||
<div onClick={handleDownload} className="system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover">{t('operation.download', { ns: 'common' })}</div>
|
||||
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target="_blank" className="system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover">{t('operation.viewDetails', { ns: 'common' })}</a>
|
||||
<a href={getMarketplaceUrl(`/plugin/${author}/${name}`, { theme })} target="_blank" className="system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover">{t('operation.viewDetails', { ns: 'common' })}</a>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
|
||||
@ -93,7 +93,7 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
|
||||
modalBottomLeft={(
|
||||
<Link
|
||||
className="flex items-center justify-center gap-1"
|
||||
href={getMarketplaceUrl(`/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)}
|
||||
href={getMarketplaceUrl(`/plugin/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)}
|
||||
target="_blank"
|
||||
>
|
||||
<span className="system-xs-regular text-xs text-text-accent">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user