mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 18:03:16 +08:00
refactor: marketplace state management (#30702)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
||||
PRICING_MODAL_QUERY_PARAM,
|
||||
PRICING_MODAL_QUERY_VALUE,
|
||||
useAccountSettingModal,
|
||||
useMarketplaceFilters,
|
||||
usePluginInstallation,
|
||||
usePricingModal,
|
||||
} from './use-query-params'
|
||||
@@ -302,174 +301,6 @@ describe('useQueryParams hooks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Marketplace filters query behavior.
|
||||
describe('useMarketplaceFilters', () => {
|
||||
it('should return default filters when query params are missing', () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(() => useMarketplaceFilters())
|
||||
|
||||
// Act
|
||||
const [filters] = result.current
|
||||
|
||||
// Assert
|
||||
expect(filters.q).toBe('')
|
||||
expect(filters.category).toBe('all')
|
||||
expect(filters.tags).toEqual([])
|
||||
})
|
||||
|
||||
it('should parse filters when query params are present', () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(
|
||||
() => useMarketplaceFilters(),
|
||||
'?q=prompt&category=tool&tags=ai,ml',
|
||||
)
|
||||
|
||||
// Act
|
||||
const [filters] = result.current
|
||||
|
||||
// Assert
|
||||
expect(filters.q).toBe('prompt')
|
||||
expect(filters.category).toBe('tool')
|
||||
expect(filters.tags).toEqual(['ai', 'ml'])
|
||||
})
|
||||
|
||||
it('should treat empty tags param as empty array', () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(
|
||||
() => useMarketplaceFilters(),
|
||||
'?tags=',
|
||||
)
|
||||
|
||||
// Act
|
||||
const [filters] = result.current
|
||||
|
||||
// Assert
|
||||
expect(filters.tags).toEqual([])
|
||||
})
|
||||
|
||||
it('should preserve other filters when updating a single field', async () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(
|
||||
() => useMarketplaceFilters(),
|
||||
'?category=tool&tags=ai,ml',
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ q: 'search' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(result.current[0].q).toBe('search'))
|
||||
expect(result.current[0].category).toBe('tool')
|
||||
expect(result.current[0].tags).toEqual(['ai', 'ml'])
|
||||
})
|
||||
|
||||
it('should clear q param when q is empty', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useMarketplaceFilters(),
|
||||
'?q=search',
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ q: '' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
expect(update.searchParams.has('q')).toBe(false)
|
||||
})
|
||||
|
||||
it('should serialize tags as comma-separated values', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(() => useMarketplaceFilters())
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ tags: ['ai', 'ml'] })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
expect(update.searchParams.get('tags')).toBe('ai,ml')
|
||||
})
|
||||
|
||||
it('should remove tags param when list is empty', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useMarketplaceFilters(),
|
||||
'?tags=ai,ml',
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ tags: [] })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
expect(update.searchParams.has('tags')).toBe(false)
|
||||
})
|
||||
|
||||
it('should keep category in the URL when set to default', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useMarketplaceFilters(),
|
||||
'?category=tool',
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ category: 'all' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
expect(update.searchParams.get('category')).toBe('all')
|
||||
})
|
||||
|
||||
it('should clear all marketplace filters when set to null', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useMarketplaceFilters(),
|
||||
'?q=search&category=tool&tags=ai,ml',
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1](null)
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
expect(update.searchParams.has('q')).toBe(false)
|
||||
expect(update.searchParams.has('category')).toBe(false)
|
||||
expect(update.searchParams.has('tags')).toBe(false)
|
||||
})
|
||||
|
||||
it('should use replace history when updating filters', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(() => useMarketplaceFilters())
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ q: 'search' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
||||
expect(update.options.history).toBe('replace')
|
||||
})
|
||||
})
|
||||
|
||||
// Plugin installation query behavior.
|
||||
describe('usePluginInstallation', () => {
|
||||
it('should parse package ids from JSON arrays', () => {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
import {
|
||||
createParser,
|
||||
parseAsArrayOf,
|
||||
parseAsString,
|
||||
useQueryState,
|
||||
useQueryStates,
|
||||
@@ -93,39 +92,6 @@ export function useAccountSettingModal<T extends string = string>() {
|
||||
return [{ isOpen, payload: currentTab }, setState] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Marketplace Search Query Parameters
|
||||
*/
|
||||
export type MarketplaceFilters = {
|
||||
q: string // search query
|
||||
category: string // plugin category
|
||||
tags: string[] // array of tags
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage marketplace search/filter state via URL
|
||||
* Provides atomic updates - all params update together
|
||||
*
|
||||
* @example
|
||||
* const [filters, setFilters] = useMarketplaceFilters()
|
||||
* setFilters({ q: 'search', category: 'tool', tags: ['ai'] }) // Updates all at once
|
||||
* setFilters({ q: '' }) // Only updates q, keeps others
|
||||
* setFilters(null) // Clears all marketplace params
|
||||
*/
|
||||
export function useMarketplaceFilters() {
|
||||
return useQueryStates(
|
||||
{
|
||||
q: parseAsString.withDefault(''),
|
||||
category: parseAsString.withDefault('all').withOptions({ clearOnDefault: false }),
|
||||
tags: parseAsArrayOf(parseAsString).withDefault([]),
|
||||
},
|
||||
{
|
||||
// Update URL without pushing to history (replaceState behavior)
|
||||
history: 'replace',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Installation Query Parameters
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user