mirror of https://github.com/langgenius/dify.git
refactor: init orpc contract (#30885)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
parent
a129e684cc
commit
91da784f84
|
|
@ -34,13 +34,6 @@ vi.mock('@/context/app-context', () => ({
|
|||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
fetchCurrentWorkspace: vi.fn(),
|
||||
fetchLangGeniusVersion: vi.fn(),
|
||||
fetchUserProfile: vi.fn(),
|
||||
getSystemFeatures: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/access-control', () => ({
|
||||
useAppWhiteListSubjects: (...args: unknown[]) => mockUseAppWhiteListSubjects(...args),
|
||||
useSearchForWhiteListCandidates: (...args: unknown[]) => mockUseSearchForWhiteListCandidates(...args),
|
||||
|
|
|
|||
|
|
@ -170,8 +170,12 @@ describe('useChatWithHistory', () => {
|
|||
await waitFor(() => {
|
||||
expect(mockFetchChatList).toHaveBeenCalledWith('conversation-1', false, 'app-1')
|
||||
})
|
||||
expect(result.current.pinnedConversationList).toEqual(pinnedData.data)
|
||||
expect(result.current.conversationList).toEqual(listData.data)
|
||||
await waitFor(() => {
|
||||
expect(result.current.pinnedConversationList).toEqual(pinnedData.data)
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(result.current.conversationList).toEqual(listData.data)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|||
import * as React from 'react'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing'
|
||||
import { fetchSubscriptionUrls } from '@/service/billing'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import Toast from '../../../../base/toast'
|
||||
import { ALL_PLANS } from '../../../config'
|
||||
import { Plan } from '../../../type'
|
||||
|
|
@ -21,10 +22,15 @@ vi.mock('@/context/app-context', () => ({
|
|||
}))
|
||||
|
||||
vi.mock('@/service/billing', () => ({
|
||||
fetchBillingUrl: vi.fn(),
|
||||
fetchSubscriptionUrls: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleClient: {
|
||||
billingUrl: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-async-window-open', () => ({
|
||||
useAsyncWindowOpen: vi.fn(),
|
||||
}))
|
||||
|
|
@ -37,7 +43,7 @@ vi.mock('../../assets', () => ({
|
|||
|
||||
const mockUseAppContext = useAppContext as Mock
|
||||
const mockUseAsyncWindowOpen = useAsyncWindowOpen as Mock
|
||||
const mockFetchBillingUrl = fetchBillingUrl as Mock
|
||||
const mockBillingUrl = consoleClient.billingUrl as Mock
|
||||
const mockFetchSubscriptionUrls = fetchSubscriptionUrls as Mock
|
||||
const mockToastNotify = Toast.notify as Mock
|
||||
|
||||
|
|
@ -69,7 +75,7 @@ beforeEach(() => {
|
|||
vi.clearAllMocks()
|
||||
mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: true })
|
||||
mockUseAsyncWindowOpen.mockReturnValue(vi.fn(async open => await open()))
|
||||
mockFetchBillingUrl.mockResolvedValue({ url: 'https://billing.example' })
|
||||
mockBillingUrl.mockResolvedValue({ url: 'https://billing.example' })
|
||||
mockFetchSubscriptionUrls.mockResolvedValue({ url: 'https://subscription.example' })
|
||||
assignedHref = ''
|
||||
})
|
||||
|
|
@ -143,7 +149,7 @@ describe('CloudPlanItem', () => {
|
|||
type: 'error',
|
||||
message: 'billing.buyPermissionDeniedTip',
|
||||
}))
|
||||
expect(mockFetchBillingUrl).not.toHaveBeenCalled()
|
||||
expect(mockBillingUrl).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open billing portal when upgrading current paid plan', async () => {
|
||||
|
|
@ -162,7 +168,7 @@ describe('CloudPlanItem', () => {
|
|||
fireEvent.click(screen.getByRole('button', { name: 'billing.plansCommon.currentPlan' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchBillingUrl).toHaveBeenCalledTimes(1)
|
||||
expect(mockBillingUrl).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(openWindow).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { useMemo } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing'
|
||||
import { fetchSubscriptionUrls } from '@/service/billing'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import Toast from '../../../../base/toast'
|
||||
import { ALL_PLANS } from '../../../config'
|
||||
import { Plan } from '../../../type'
|
||||
|
|
@ -76,7 +77,7 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
|||
try {
|
||||
if (isCurrentPaidPlan) {
|
||||
await openAsyncWindow(async () => {
|
||||
const res = await fetchBillingUrl()
|
||||
const res = await consoleClient.billingUrl()
|
||||
if (res.url)
|
||||
return res.url
|
||||
throw new Error('Failed to open billing page')
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ export const useMarketplaceAllPlugins = (providers: any[], searchText: string) =
|
|||
category: PluginCategoryEnum.datasource,
|
||||
exclude,
|
||||
type: 'plugin',
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
})
|
||||
}
|
||||
else {
|
||||
|
|
@ -39,10 +39,10 @@ export const useMarketplaceAllPlugins = (providers: any[], searchText: string) =
|
|||
query: '',
|
||||
category: PluginCategoryEnum.datasource,
|
||||
type: 'plugin',
|
||||
pageSize: 1000,
|
||||
page_size: 1000,
|
||||
exclude,
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
})
|
||||
}
|
||||
}, [queryPlugins, queryPluginsWithDebounced, searchText, exclude])
|
||||
|
|
|
|||
|
|
@ -275,8 +275,8 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
|
|||
category: PluginCategoryEnum.model,
|
||||
exclude,
|
||||
type: 'plugin',
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
})
|
||||
}
|
||||
else {
|
||||
|
|
@ -284,10 +284,10 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
|
|||
query: '',
|
||||
category: PluginCategoryEnum.model,
|
||||
type: 'plugin',
|
||||
pageSize: 1000,
|
||||
page_size: 1000,
|
||||
exclude,
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
})
|
||||
}
|
||||
}, [queryPlugins, queryPluginsWithDebounced, searchText, exclude])
|
||||
|
|
|
|||
|
|
@ -100,11 +100,11 @@ export const useMarketplacePlugins = () => {
|
|||
const [queryParams, setQueryParams] = useState<PluginsSearchParams>()
|
||||
|
||||
const normalizeParams = useCallback((pluginsSearchParams: PluginsSearchParams) => {
|
||||
const pageSize = pluginsSearchParams.pageSize || 40
|
||||
const page_size = pluginsSearchParams.page_size || 40
|
||||
|
||||
return {
|
||||
...pluginsSearchParams,
|
||||
pageSize,
|
||||
page_size,
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
@ -116,20 +116,20 @@ export const useMarketplacePlugins = () => {
|
|||
plugins: [] as Plugin[],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 40,
|
||||
page_size: 40,
|
||||
}
|
||||
}
|
||||
|
||||
const params = normalizeParams(queryParams)
|
||||
const {
|
||||
query,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category,
|
||||
tags,
|
||||
exclude,
|
||||
type,
|
||||
pageSize,
|
||||
page_size,
|
||||
} = params
|
||||
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
|
||||
|
||||
|
|
@ -137,10 +137,10 @@ export const useMarketplacePlugins = () => {
|
|||
const res = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
|
||||
body: {
|
||||
page: pageParam,
|
||||
page_size: pageSize,
|
||||
page_size,
|
||||
query,
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category: category !== 'all' ? category : '',
|
||||
tags,
|
||||
exclude,
|
||||
|
|
@ -154,7 +154,7 @@ export const useMarketplacePlugins = () => {
|
|||
plugins: resPlugins.map(plugin => getFormattedPlugin(plugin)),
|
||||
total: res.data.total,
|
||||
page: pageParam,
|
||||
pageSize,
|
||||
page_size,
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
|
@ -162,13 +162,13 @@ export const useMarketplacePlugins = () => {
|
|||
plugins: [],
|
||||
total: 0,
|
||||
page: pageParam,
|
||||
pageSize,
|
||||
page_size,
|
||||
}
|
||||
}
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const nextPage = lastPage.page + 1
|
||||
const loaded = lastPage.page * lastPage.pageSize
|
||||
const loaded = lastPage.page * lastPage.page_size
|
||||
return loaded < (lastPage.total || 0) ? nextPage : undefined
|
||||
},
|
||||
initialPageParam: 1,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ 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 { PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
|
||||
import { marketplaceKeys } from './query'
|
||||
import { marketplaceSearchParamsParsers } from './search-params'
|
||||
import { getCollectionsParams, getMarketplaceCollectionsAndPlugins } from './utils'
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ async function getDehydratedState(searchParams?: Promise<SearchParams>) {
|
|||
const queryClient = getQueryClientServer()
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: marketplaceKeys.collections(getCollectionsParams(params.category)),
|
||||
queryKey: marketplaceQuery.collections.queryKey({ input: { query: getCollectionsParams(params.category) } }),
|
||||
queryFn: () => getMarketplaceCollectionsAndPlugins(getCollectionsParams(params.category)),
|
||||
})
|
||||
return dehydrate(queryClient)
|
||||
|
|
|
|||
|
|
@ -60,10 +60,10 @@ vi.mock('@/service/use-plugins', () => ({
|
|||
// Mock tanstack query
|
||||
const mockFetchNextPage = vi.fn()
|
||||
const mockHasNextPage = false
|
||||
let mockInfiniteQueryData: { pages: Array<{ plugins: unknown[], total: number, page: number, pageSize: number }> } | undefined
|
||||
let mockInfiniteQueryData: { pages: Array<{ plugins: unknown[], total: number, page: number, page_size: number }> } | undefined
|
||||
let capturedInfiniteQueryFn: ((ctx: { pageParam: number, signal: AbortSignal }) => Promise<unknown>) | null = null
|
||||
let capturedQueryFn: ((ctx: { signal: AbortSignal }) => Promise<unknown>) | null = null
|
||||
let capturedGetNextPageParam: ((lastPage: { page: number, pageSize: number, total: number }) => number | undefined) | null = null
|
||||
let capturedGetNextPageParam: ((lastPage: { page: number, page_size: number, total: number }) => number | undefined) | null = null
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: vi.fn(({ queryFn, enabled }: { queryFn: (ctx: { signal: AbortSignal }) => Promise<unknown>, enabled: boolean }) => {
|
||||
|
|
@ -83,7 +83,7 @@ vi.mock('@tanstack/react-query', () => ({
|
|||
}),
|
||||
useInfiniteQuery: vi.fn(({ queryFn, getNextPageParam, enabled: _enabled }: {
|
||||
queryFn: (ctx: { pageParam: number, signal: AbortSignal }) => Promise<unknown>
|
||||
getNextPageParam: (lastPage: { page: number, pageSize: number, total: number }) => number | undefined
|
||||
getNextPageParam: (lastPage: { page: number, page_size: number, total: number }) => number | undefined
|
||||
enabled: boolean
|
||||
}) => {
|
||||
// Capture queryFn and getNextPageParam for later testing
|
||||
|
|
@ -97,9 +97,9 @@ vi.mock('@tanstack/react-query', () => ({
|
|||
// Call getNextPageParam to increase coverage
|
||||
if (getNextPageParam) {
|
||||
// Test with more data available
|
||||
getNextPageParam({ page: 1, pageSize: 40, total: 100 })
|
||||
getNextPageParam({ page: 1, page_size: 40, total: 100 })
|
||||
// Test with no more data
|
||||
getNextPageParam({ page: 3, pageSize: 40, total: 100 })
|
||||
getNextPageParam({ page: 3, page_size: 40, total: 100 })
|
||||
}
|
||||
return {
|
||||
data: mockInfiniteQueryData,
|
||||
|
|
@ -151,6 +151,7 @@ vi.mock('@/service/base', () => ({
|
|||
|
||||
// Mock config
|
||||
vi.mock('@/config', () => ({
|
||||
API_PREFIX: '/api',
|
||||
APP_VERSION: '1.0.0',
|
||||
IS_MARKETPLACE: false,
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.dify.ai/api/v1',
|
||||
|
|
@ -731,10 +732,10 @@ describe('useMarketplacePlugins', () => {
|
|||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
category: 'tool',
|
||||
pageSize: 20,
|
||||
page_size: 20,
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
|
@ -747,7 +748,7 @@ describe('useMarketplacePlugins', () => {
|
|||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
type: 'bundle',
|
||||
pageSize: 40,
|
||||
page_size: 40,
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
|
@ -798,8 +799,8 @@ describe('useMarketplacePlugins', () => {
|
|||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
category: 'all',
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
|
@ -824,7 +825,7 @@ describe('useMarketplacePlugins', () => {
|
|||
expect(() => {
|
||||
result.current.queryPlugins({
|
||||
query: 'test',
|
||||
pageSize: 100,
|
||||
page_size: 100,
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
|
@ -843,7 +844,7 @@ describe('Hooks queryFn Coverage', () => {
|
|||
// Set mock data to have pages
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [{ name: 'plugin1' }], total: 10, page: 1, pageSize: 40 },
|
||||
{ plugins: [{ name: 'plugin1' }], total: 10, page: 1, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -863,8 +864,8 @@ describe('Hooks queryFn Coverage', () => {
|
|||
it('should expose page and total from infinite query data', async () => {
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [{ name: 'plugin1' }, { name: 'plugin2' }], total: 20, page: 1, pageSize: 40 },
|
||||
{ plugins: [{ name: 'plugin3' }], total: 20, page: 2, pageSize: 40 },
|
||||
{ plugins: [{ name: 'plugin1' }, { name: 'plugin2' }], total: 20, page: 1, page_size: 40 },
|
||||
{ plugins: [{ name: 'plugin3' }], total: 20, page: 2, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -893,7 +894,7 @@ describe('Hooks queryFn Coverage', () => {
|
|||
it('should return total from first page when query is set and data exists', async () => {
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [], total: 50, page: 1, pageSize: 40 },
|
||||
{ plugins: [], total: 50, page: 1, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -917,8 +918,8 @@ describe('Hooks queryFn Coverage', () => {
|
|||
type: 'plugin',
|
||||
query: 'search test',
|
||||
category: 'model',
|
||||
sortBy: 'version_updated_at',
|
||||
sortOrder: 'ASC',
|
||||
sort_by: 'version_updated_at',
|
||||
sort_order: 'ASC',
|
||||
})
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
|
|
@ -1027,13 +1028,13 @@ describe('Advanced Hook Integration', () => {
|
|||
// Test with all possible parameters
|
||||
result.current.queryPlugins({
|
||||
query: 'comprehensive test',
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
category: 'tool',
|
||||
tags: ['tag1', 'tag2'],
|
||||
exclude: ['excluded-plugin'],
|
||||
type: 'plugin',
|
||||
pageSize: 50,
|
||||
page_size: 50,
|
||||
})
|
||||
|
||||
expect(result.current).toBeDefined()
|
||||
|
|
@ -1081,9 +1082,9 @@ describe('Direct queryFn Coverage', () => {
|
|||
result.current.queryPlugins({
|
||||
query: 'direct test',
|
||||
category: 'tool',
|
||||
sortBy: 'install_count',
|
||||
sortOrder: 'DESC',
|
||||
pageSize: 40,
|
||||
sort_by: 'install_count',
|
||||
sort_order: 'DESC',
|
||||
page_size: 40,
|
||||
})
|
||||
|
||||
// Now queryFn should be captured and enabled
|
||||
|
|
@ -1255,7 +1256,7 @@ describe('Direct queryFn Coverage', () => {
|
|||
|
||||
result.current.queryPlugins({
|
||||
query: 'structure test',
|
||||
pageSize: 20,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
if (capturedInfiniteQueryFn) {
|
||||
|
|
@ -1264,14 +1265,14 @@ describe('Direct queryFn Coverage', () => {
|
|||
plugins: unknown[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// Verify the returned structure
|
||||
expect(response).toHaveProperty('plugins')
|
||||
expect(response).toHaveProperty('total')
|
||||
expect(response).toHaveProperty('page')
|
||||
expect(response).toHaveProperty('pageSize')
|
||||
expect(response).toHaveProperty('page_size')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -1296,7 +1297,7 @@ describe('flatMap Coverage', () => {
|
|||
],
|
||||
total: 5,
|
||||
page: 1,
|
||||
pageSize: 40,
|
||||
page_size: 40,
|
||||
},
|
||||
{
|
||||
plugins: [
|
||||
|
|
@ -1304,7 +1305,7 @@ describe('flatMap Coverage', () => {
|
|||
],
|
||||
total: 5,
|
||||
page: 2,
|
||||
pageSize: 40,
|
||||
page_size: 40,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1336,8 +1337,8 @@ describe('flatMap Coverage', () => {
|
|||
it('should test hook with pages data for flatMap path', async () => {
|
||||
mockInfiniteQueryData = {
|
||||
pages: [
|
||||
{ plugins: [], total: 100, page: 1, pageSize: 40 },
|
||||
{ plugins: [], total: 100, page: 2, pageSize: 40 },
|
||||
{ plugins: [], total: 100, page: 1, page_size: 40 },
|
||||
{ plugins: [], total: 100, page: 2, page_size: 40 },
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -1371,7 +1372,7 @@ describe('flatMap Coverage', () => {
|
|||
plugins: unknown[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
page_size: number
|
||||
}
|
||||
// When error is caught, should return fallback data
|
||||
expect(response.plugins).toEqual([])
|
||||
|
|
@ -1392,15 +1393,15 @@ describe('flatMap Coverage', () => {
|
|||
// Test getNextPageParam function directly
|
||||
if (capturedGetNextPageParam) {
|
||||
// When there are more pages
|
||||
const nextPage = capturedGetNextPageParam({ page: 1, pageSize: 40, total: 100 })
|
||||
const nextPage = capturedGetNextPageParam({ page: 1, page_size: 40, total: 100 })
|
||||
expect(nextPage).toBe(2)
|
||||
|
||||
// When all data is loaded
|
||||
const noMorePages = capturedGetNextPageParam({ page: 3, pageSize: 40, total: 100 })
|
||||
const noMorePages = capturedGetNextPageParam({ page: 3, page_size: 40, total: 100 })
|
||||
expect(noMorePages).toBeUndefined()
|
||||
|
||||
// Edge case: exactly at boundary
|
||||
const atBoundary = capturedGetNextPageParam({ page: 2, pageSize: 50, total: 100 })
|
||||
const atBoundary = capturedGetNextPageParam({ page: 2, page_size: 50, total: 100 })
|
||||
expect(atBoundary).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
|
@ -1427,7 +1428,7 @@ describe('flatMap Coverage', () => {
|
|||
plugins: unknown[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
page_size: number
|
||||
}
|
||||
// Catch block should return fallback values
|
||||
expect(response.plugins).toEqual([])
|
||||
|
|
@ -1446,7 +1447,7 @@ describe('flatMap Coverage', () => {
|
|||
plugins: [{ name: 'test-plugin-1' }, { name: 'test-plugin-2' }],
|
||||
total: 10,
|
||||
page: 1,
|
||||
pageSize: 40,
|
||||
page_size: 40,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1489,9 +1490,12 @@ describe('Async Utils', () => {
|
|||
{ type: 'plugin', org: 'test', name: 'plugin2' },
|
||||
]
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
json: () => Promise.resolve({ data: { plugins: mockPlugins } }),
|
||||
})
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
const result = await getMarketplacePluginsByCollectionId('test-collection', {
|
||||
|
|
@ -1514,19 +1518,26 @@ describe('Async Utils', () => {
|
|||
})
|
||||
|
||||
it('should pass abort signal when provided', async () => {
|
||||
const mockPlugins = [{ type: 'plugin', org: 'test', name: 'plugin1' }]
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
json: () => Promise.resolve({ data: { plugins: mockPlugins } }),
|
||||
})
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1' }]
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
|
||||
const controller = new AbortController()
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
await getMarketplacePluginsByCollectionId('test-collection', {}, { signal: controller.signal })
|
||||
|
||||
// oRPC uses Request objects, so check that fetch was called with a Request containing the right URL
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ signal: controller.signal }),
|
||||
expect.any(Request),
|
||||
expect.any(Object),
|
||||
)
|
||||
const call = vi.mocked(globalThis.fetch).mock.calls[0]
|
||||
const request = call[0] as Request
|
||||
expect(request.url).toContain('test-collection')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1535,19 +1546,25 @@ describe('Async Utils', () => {
|
|||
const mockCollections = [
|
||||
{ name: 'collection1', label: {}, description: {}, rule: '', created_at: '', updated_at: '' },
|
||||
]
|
||||
const mockPlugins = [{ type: 'plugin', org: 'test', name: 'plugin1' }]
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1' }]
|
||||
|
||||
let callCount = 0
|
||||
globalThis.fetch = vi.fn().mockImplementation(() => {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve({ data: { collections: mockCollections } }),
|
||||
})
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { collections: mockCollections } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve({ data: { plugins: mockPlugins } }),
|
||||
})
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
|
|
@ -1571,9 +1588,12 @@ describe('Async Utils', () => {
|
|||
})
|
||||
|
||||
it('should append condition and type to URL when provided', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
json: () => Promise.resolve({ data: { collections: [] } }),
|
||||
})
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { collections: [] } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
await getMarketplaceCollectionsAndPlugins({
|
||||
|
|
@ -1581,10 +1601,11 @@ describe('Async Utils', () => {
|
|||
type: 'bundle',
|
||||
})
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('condition=category=tool'),
|
||||
expect.any(Object),
|
||||
)
|
||||
// oRPC uses Request objects, so check that fetch was called with a Request containing the right URL
|
||||
expect(globalThis.fetch).toHaveBeenCalled()
|
||||
const call = vi.mocked(globalThis.fetch).mock.calls[0]
|
||||
const request = call[0] as Request
|
||||
expect(request.url).toContain('condition=category%3Dtool')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
import type { CollectionsAndPluginsSearchParams, PluginsSearchParams } from './types'
|
||||
import type { PluginsSearchParams } from './types'
|
||||
import type { MarketPlaceInputs } from '@/contract/router'
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { marketplaceQuery } from '@/service/client'
|
||||
import { getMarketplaceCollectionsAndPlugins, getMarketplacePlugins } from './utils'
|
||||
|
||||
// TODO: Avoid manual maintenance of query keys and better service management,
|
||||
// https://github.com/langgenius/dify/issues/30342
|
||||
|
||||
export const marketplaceKeys = {
|
||||
all: ['marketplace'] as const,
|
||||
collections: (params?: CollectionsAndPluginsSearchParams) => [...marketplaceKeys.all, 'collections', params] as const,
|
||||
collectionPlugins: (collectionId: string, params?: CollectionsAndPluginsSearchParams) => [...marketplaceKeys.all, 'collectionPlugins', collectionId, params] as const,
|
||||
plugins: (params?: PluginsSearchParams) => [...marketplaceKeys.all, 'plugins', params] as const,
|
||||
}
|
||||
|
||||
export function useMarketplaceCollectionsAndPlugins(
|
||||
collectionsParams: CollectionsAndPluginsSearchParams,
|
||||
collectionsParams: MarketPlaceInputs['collections']['query'],
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: marketplaceKeys.collections(collectionsParams),
|
||||
queryKey: marketplaceQuery.collections.queryKey({ input: { query: collectionsParams } }),
|
||||
queryFn: ({ signal }) => getMarketplaceCollectionsAndPlugins(collectionsParams, { signal }),
|
||||
})
|
||||
}
|
||||
|
|
@ -25,11 +17,16 @@ export function useMarketplacePlugins(
|
|||
queryParams: PluginsSearchParams | undefined,
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: marketplaceKeys.plugins(queryParams),
|
||||
queryKey: marketplaceQuery.searchAdvanced.queryKey({
|
||||
input: {
|
||||
body: queryParams!,
|
||||
params: { kind: queryParams?.type === 'bundle' ? 'bundles' : 'plugins' },
|
||||
},
|
||||
}),
|
||||
queryFn: ({ pageParam = 1, signal }) => getMarketplacePlugins(queryParams, pageParam, signal),
|
||||
getNextPageParam: (lastPage) => {
|
||||
const nextPage = lastPage.page + 1
|
||||
const loaded = lastPage.page * lastPage.pageSize
|
||||
const loaded = lastPage.page * lastPage.page_size
|
||||
return loaded < (lastPage.total || 0) ? nextPage : undefined
|
||||
},
|
||||
initialPageParam: 1,
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ export function useMarketplaceData() {
|
|||
query: searchPluginText,
|
||||
category: activePluginType === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginType,
|
||||
tags: filterPluginTags,
|
||||
sortBy: sort.sortBy,
|
||||
sortOrder: sort.sortOrder,
|
||||
sort_by: sort.sortBy,
|
||||
sort_order: sort.sortOrder,
|
||||
type: getMarketplaceListFilterType(activePluginType),
|
||||
}
|
||||
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ export type MarketplaceCollectionPluginsResponse = {
|
|||
export type PluginsSearchParams = {
|
||||
query: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
sortBy?: string
|
||||
sortOrder?: string
|
||||
page_size?: number
|
||||
sort_by?: string
|
||||
sort_order?: string
|
||||
category?: string
|
||||
tags?: string[]
|
||||
exclude?: string[]
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import type {
|
|||
MarketplaceCollection,
|
||||
PluginsSearchParams,
|
||||
} from '@/app/components/plugins/marketplace/types'
|
||||
import type { Plugin, PluginsFromMarketplaceResponse } from '@/app/components/plugins/types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import {
|
||||
APP_VERSION,
|
||||
IS_MARKETPLACE,
|
||||
MARKETPLACE_API_PREFIX,
|
||||
} from '@/config'
|
||||
import { postMarketplace } from '@/service/base'
|
||||
import { marketplaceClient } from '@/service/client'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { PLUGIN_TYPE_SEARCH_MAP } from './constants'
|
||||
|
||||
|
|
@ -19,10 +17,6 @@ type MarketplaceFetchOptions = {
|
|||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
const getMarketplaceHeaders = () => new Headers({
|
||||
'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0',
|
||||
})
|
||||
|
||||
export const getPluginIconInMarketplace = (plugin: Plugin) => {
|
||||
if (plugin.type === 'bundle')
|
||||
return `${MARKETPLACE_API_PREFIX}/bundles/${plugin.org}/${plugin.name}/icon`
|
||||
|
|
@ -65,24 +59,15 @@ export const getMarketplacePluginsByCollectionId = async (
|
|||
let plugins: Plugin[] = []
|
||||
|
||||
try {
|
||||
const url = `${MARKETPLACE_API_PREFIX}/collections/${collectionId}/plugins`
|
||||
const headers = getMarketplaceHeaders()
|
||||
const marketplaceCollectionPluginsData = await globalThis.fetch(
|
||||
url,
|
||||
{
|
||||
cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers,
|
||||
signal: options?.signal,
|
||||
body: JSON.stringify({
|
||||
category: query?.category,
|
||||
exclude: query?.exclude,
|
||||
type: query?.type,
|
||||
}),
|
||||
const marketplaceCollectionPluginsDataJson = await marketplaceClient.collectionPlugins({
|
||||
params: {
|
||||
collectionId,
|
||||
},
|
||||
)
|
||||
const marketplaceCollectionPluginsDataJson = await marketplaceCollectionPluginsData.json()
|
||||
plugins = (marketplaceCollectionPluginsDataJson.data.plugins || []).map((plugin: Plugin) => getFormattedPlugin(plugin))
|
||||
body: query,
|
||||
}, {
|
||||
signal: options?.signal,
|
||||
})
|
||||
plugins = (marketplaceCollectionPluginsDataJson.data?.plugins || []).map(plugin => getFormattedPlugin(plugin))
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (e) {
|
||||
|
|
@ -99,22 +84,16 @@ export const getMarketplaceCollectionsAndPlugins = async (
|
|||
let marketplaceCollections: MarketplaceCollection[] = []
|
||||
let marketplaceCollectionPluginsMap: Record<string, Plugin[]> = {}
|
||||
try {
|
||||
let marketplaceUrl = `${MARKETPLACE_API_PREFIX}/collections?page=1&page_size=100`
|
||||
if (query?.condition)
|
||||
marketplaceUrl += `&condition=${query.condition}`
|
||||
if (query?.type)
|
||||
marketplaceUrl += `&type=${query.type}`
|
||||
const headers = getMarketplaceHeaders()
|
||||
const marketplaceCollectionsData = await globalThis.fetch(
|
||||
marketplaceUrl,
|
||||
{
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
signal: options?.signal,
|
||||
const marketplaceCollectionsDataJson = await marketplaceClient.collections({
|
||||
query: {
|
||||
...query,
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
},
|
||||
)
|
||||
const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json()
|
||||
marketplaceCollections = marketplaceCollectionsDataJson.data.collections || []
|
||||
}, {
|
||||
signal: options?.signal,
|
||||
})
|
||||
marketplaceCollections = marketplaceCollectionsDataJson.data?.collections || []
|
||||
await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => {
|
||||
const plugins = await getMarketplacePluginsByCollectionId(collection.name, query, options)
|
||||
|
||||
|
|
@ -143,42 +122,42 @@ export const getMarketplacePlugins = async (
|
|||
plugins: [] as Plugin[],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 40,
|
||||
page_size: 40,
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
query,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category,
|
||||
tags,
|
||||
type,
|
||||
pageSize = 40,
|
||||
page_size = 40,
|
||||
} = queryParams
|
||||
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
|
||||
|
||||
try {
|
||||
const res = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
|
||||
const res = await marketplaceClient.searchAdvanced({
|
||||
params: {
|
||||
kind: type === 'bundle' ? 'bundles' : 'plugins',
|
||||
},
|
||||
body: {
|
||||
page: pageParam,
|
||||
page_size: pageSize,
|
||||
page_size,
|
||||
query,
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category: category !== 'all' ? category : '',
|
||||
tags,
|
||||
type,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
}, { signal })
|
||||
const resPlugins = res.data.bundles || res.data.plugins || []
|
||||
|
||||
return {
|
||||
plugins: resPlugins.map(plugin => getFormattedPlugin(plugin)),
|
||||
total: res.data.total,
|
||||
page: pageParam,
|
||||
pageSize,
|
||||
page_size,
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
|
@ -186,7 +165,7 @@ export const getMarketplacePlugins = async (
|
|||
plugins: [],
|
||||
total: 0,
|
||||
page: pageParam,
|
||||
pageSize,
|
||||
page_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ vi.mock('@/service/common', () => ({
|
|||
fetchInitValidateStatus: vi.fn(),
|
||||
setup: vi.fn(),
|
||||
login: vi.fn(),
|
||||
getSystemFeatures: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/global-public-context', async (importOriginal) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { SystemFeatures } from '@/types/feature'
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { create } from 'zustand'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { getSystemFeatures } from '@/service/common'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ const systemFeaturesQueryKey = ['systemFeatures'] as const
|
|||
const setupStatusQueryKey = ['setupStatus'] as const
|
||||
|
||||
async function fetchSystemFeatures() {
|
||||
const data = await getSystemFeatures()
|
||||
const data = await consoleClient.systemFeatures()
|
||||
const { setSystemFeatures } = useGlobalPublicStore.getState()
|
||||
setSystemFeatures({ ...defaultSystemFeatures, ...data })
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { oc } from '@orpc/contract'
|
||||
|
||||
export const base = oc.$route({ inputStructure: 'detailed' })
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from './base'
|
||||
|
||||
export const systemFeaturesContract = base
|
||||
.route({
|
||||
path: '/system-features',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<unknown>())
|
||||
.output(type<SystemFeatures>())
|
||||
|
||||
export const billingUrlContract = base
|
||||
.route({
|
||||
path: '/billing/invoices',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<unknown>())
|
||||
.output(type<{ url: string }>())
|
||||
|
||||
export const bindPartnerStackContract = base
|
||||
.route({
|
||||
path: '/billing/partners/{partnerKey}/tenants',
|
||||
method: 'PUT',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
partnerKey: string
|
||||
}
|
||||
body: {
|
||||
click_id: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type { CollectionsAndPluginsSearchParams, MarketplaceCollection, PluginsSearchParams } from '@/app/components/plugins/marketplace/types'
|
||||
import type { Plugin, PluginsFromMarketplaceResponse } from '@/app/components/plugins/types'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from './base'
|
||||
|
||||
export const collectionsContract = base
|
||||
.route({
|
||||
path: '/collections',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(
|
||||
type<{
|
||||
query?: CollectionsAndPluginsSearchParams & { page?: number, page_size?: number }
|
||||
}>(),
|
||||
)
|
||||
.output(
|
||||
type<{
|
||||
data?: {
|
||||
collections?: MarketplaceCollection[]
|
||||
}
|
||||
}>(),
|
||||
)
|
||||
|
||||
export const collectionPluginsContract = base
|
||||
.route({
|
||||
path: '/collections/{collectionId}/plugins',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(
|
||||
type<{
|
||||
params: {
|
||||
collectionId: string
|
||||
}
|
||||
body?: CollectionsAndPluginsSearchParams
|
||||
}>(),
|
||||
)
|
||||
.output(
|
||||
type<{
|
||||
data?: {
|
||||
plugins?: Plugin[]
|
||||
}
|
||||
}>(),
|
||||
)
|
||||
|
||||
export const searchAdvancedContract = base
|
||||
.route({
|
||||
path: '/{kind}/search/advanced',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
kind: 'plugins' | 'bundles'
|
||||
}
|
||||
body: Omit<PluginsSearchParams, 'type'>
|
||||
}>())
|
||||
.output(type<{ data: PluginsFromMarketplaceResponse }>())
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import type { InferContractRouterInputs } from '@orpc/contract'
|
||||
import { billingUrlContract, bindPartnerStackContract, systemFeaturesContract } from './console'
|
||||
import { collectionPluginsContract, collectionsContract, searchAdvancedContract } from './marketplace'
|
||||
|
||||
export const marketplaceRouterContract = {
|
||||
collections: collectionsContract,
|
||||
collectionPlugins: collectionPluginsContract,
|
||||
searchAdvanced: searchAdvancedContract,
|
||||
}
|
||||
|
||||
export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRouterContract>
|
||||
|
||||
export const consoleRouterContract = {
|
||||
systemFeatures: systemFeaturesContract,
|
||||
billingUrl: billingUrlContract,
|
||||
bindPartnerStack: bindPartnerStackContract,
|
||||
}
|
||||
|
||||
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
|
||||
|
|
@ -23,10 +23,6 @@ vi.mock('@/context/global-public-context', async (importOriginal) => {
|
|||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
getSystemFeatures: vi.fn(() => ({ ...defaultSystemFeatures })),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Test behavior when system features are still loading
|
||||
* Title should remain empty to prevent flicker
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@
|
|||
"@monaco-editor/react": "^4.7.0",
|
||||
"@octokit/core": "^6.1.6",
|
||||
"@octokit/request-error": "^6.1.8",
|
||||
"@orpc/client": "^1.13.4",
|
||||
"@orpc/contract": "^1.13.4",
|
||||
"@orpc/openapi-client": "^1.13.4",
|
||||
"@orpc/tanstack-query": "^1.13.4",
|
||||
"@remixicon/react": "^4.7.0",
|
||||
"@sentry/react": "^8.55.0",
|
||||
"@svgdotjs/svg.js": "^3.2.5",
|
||||
|
|
|
|||
|
|
@ -108,6 +108,18 @@ importers:
|
|||
'@octokit/request-error':
|
||||
specifier: ^6.1.8
|
||||
version: 6.1.8
|
||||
'@orpc/client':
|
||||
specifier: ^1.13.4
|
||||
version: 1.13.4
|
||||
'@orpc/contract':
|
||||
specifier: ^1.13.4
|
||||
version: 1.13.4
|
||||
'@orpc/openapi-client':
|
||||
specifier: ^1.13.4
|
||||
version: 1.13.4
|
||||
'@orpc/tanstack-query':
|
||||
specifier: ^1.13.4
|
||||
version: 1.13.4(@orpc/client@1.13.4)(@tanstack/query-core@5.90.12)
|
||||
'@remixicon/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(react@19.2.3)
|
||||
|
|
@ -2291,6 +2303,38 @@ packages:
|
|||
'@open-draft/until@2.1.0':
|
||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||
|
||||
'@orpc/client@1.13.4':
|
||||
resolution: {integrity: sha512-s13GPMeoooJc5Th2EaYT5HMFtWG8S03DUVytYfJv8pIhP87RYKl94w52A36denH6r/B4LaAgBeC9nTAOslK+Og==}
|
||||
|
||||
'@orpc/contract@1.13.4':
|
||||
resolution: {integrity: sha512-TIxyaF67uOlihCRcasjHZxguZpbqfNK7aMrDLnhoufmQBE4OKvguNzmrOFHgsuM0OXoopX0Nuhun1ccaxKP10A==}
|
||||
|
||||
'@orpc/openapi-client@1.13.4':
|
||||
resolution: {integrity: sha512-tRUcY4E6sgpS5bY/9nNES/Q/PMyYyPOsI4TuhwLhfgxOb0GFPwYKJ6Kif7KFNOhx4fkN/jTOfE1nuWuIZU1gyg==}
|
||||
|
||||
'@orpc/shared@1.13.4':
|
||||
resolution: {integrity: sha512-TYt9rLG/BUkNQBeQ6C1tEiHS/Seb8OojHgj9GlvqyjHJhMZx5qjsIyTW6RqLPZJ4U2vgK6x4Her36+tlFCKJug==}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.9.0'
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
|
||||
'@orpc/standard-server-fetch@1.13.4':
|
||||
resolution: {integrity: sha512-/zmKwnuxfAXbppJpgr1CMnQX3ptPlYcDzLz1TaVzz9VG/Xg58Ov3YhabS2Oi1utLVhy5t4kaCppUducAvoKN+A==}
|
||||
|
||||
'@orpc/standard-server-peer@1.13.4':
|
||||
resolution: {integrity: sha512-UfqnTLqevjCKUk4cmImOG8cQUwANpV1dp9e9u2O1ki6BRBsg/zlXFg6G2N6wP0zr9ayIiO1d2qJdH55yl/1BNw==}
|
||||
|
||||
'@orpc/standard-server@1.13.4':
|
||||
resolution: {integrity: sha512-ZOzgfVp6XUg+wVYw+gqesfRfGPtQbnBIrIiSnFMtZF+6ncmFJeF2Shc4RI2Guqc0Qz25juy8Ogo4tX3YqysOcg==}
|
||||
|
||||
'@orpc/tanstack-query@1.13.4':
|
||||
resolution: {integrity: sha512-gCL/kh3kf6OUGKfXxSoOZpcX1jNYzxGfo/PkLQKX7ui4xiTbfWw3sCDF30sNS4I7yAOnBwDwJ3N2xzfkTftOBg==}
|
||||
peerDependencies:
|
||||
'@orpc/client': 1.13.4
|
||||
'@tanstack/query-core': '>=5.80.2'
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.15.0':
|
||||
resolution: {integrity: sha512-Q+lWuFfq7whNelNJIP1dhXaVz4zO9Tu77GcQHyxDWh3MaCoO2Bisphgzmsh4ZoUe2zIchQh6OvQL99GlWHg9Tw==}
|
||||
cpu: [arm]
|
||||
|
|
@ -6685,6 +6729,9 @@ packages:
|
|||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
opener@1.5.2:
|
||||
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||
hasBin: true
|
||||
|
|
@ -7081,6 +7128,10 @@ packages:
|
|||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
radash@12.1.1:
|
||||
resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
|
|
@ -7826,6 +7877,10 @@ packages:
|
|||
tabbable@6.3.0:
|
||||
resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==}
|
||||
|
||||
tagged-tag@1.0.0:
|
||||
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||
|
||||
|
|
@ -8027,13 +8082,17 @@ packages:
|
|||
resolution: {integrity: sha512-5zknd7Dss75pMSED270A1RQS3KloqRJA9XbXLe0eCxyw7xXFb3rd+9B0UQ/0E+LQT6lnrLviEolYORlRWamn4w==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
type-fest@5.4.0:
|
||||
resolution: {integrity: sha512-wfkA6r0tBpVfGiyO+zbf9e10QkRQSlK9F2UvyfnjoCmrvH2bjHyhPzhugSBOuq1dog3P0+FKckqe+Xf6WKVjwg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
ufo@1.6.1:
|
||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||
ufo@1.6.2:
|
||||
resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==}
|
||||
|
||||
uglify-js@3.19.3:
|
||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||
|
|
@ -10638,6 +10697,66 @@ snapshots:
|
|||
|
||||
'@open-draft/until@2.1.0': {}
|
||||
|
||||
'@orpc/client@1.13.4':
|
||||
dependencies:
|
||||
'@orpc/shared': 1.13.4
|
||||
'@orpc/standard-server': 1.13.4
|
||||
'@orpc/standard-server-fetch': 1.13.4
|
||||
'@orpc/standard-server-peer': 1.13.4
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
|
||||
'@orpc/contract@1.13.4':
|
||||
dependencies:
|
||||
'@orpc/client': 1.13.4
|
||||
'@orpc/shared': 1.13.4
|
||||
'@standard-schema/spec': 1.1.0
|
||||
openapi-types: 12.1.3
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
|
||||
'@orpc/openapi-client@1.13.4':
|
||||
dependencies:
|
||||
'@orpc/client': 1.13.4
|
||||
'@orpc/contract': 1.13.4
|
||||
'@orpc/shared': 1.13.4
|
||||
'@orpc/standard-server': 1.13.4
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
|
||||
'@orpc/shared@1.13.4':
|
||||
dependencies:
|
||||
radash: 12.1.1
|
||||
type-fest: 5.4.0
|
||||
|
||||
'@orpc/standard-server-fetch@1.13.4':
|
||||
dependencies:
|
||||
'@orpc/shared': 1.13.4
|
||||
'@orpc/standard-server': 1.13.4
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
|
||||
'@orpc/standard-server-peer@1.13.4':
|
||||
dependencies:
|
||||
'@orpc/shared': 1.13.4
|
||||
'@orpc/standard-server': 1.13.4
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
|
||||
'@orpc/standard-server@1.13.4':
|
||||
dependencies:
|
||||
'@orpc/shared': 1.13.4
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
|
||||
'@orpc/tanstack-query@1.13.4(@orpc/client@1.13.4)(@tanstack/query-core@5.90.12)':
|
||||
dependencies:
|
||||
'@orpc/client': 1.13.4
|
||||
'@orpc/shared': 1.13.4
|
||||
'@tanstack/query-core': 5.90.12
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.15.0':
|
||||
optional: true
|
||||
|
||||
|
|
@ -15603,7 +15722,7 @@ snapshots:
|
|||
acorn: 8.15.0
|
||||
pathe: 2.0.3
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.1
|
||||
ufo: 1.6.2
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
|
|
@ -15766,6 +15885,8 @@ snapshots:
|
|||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
openapi-types@12.1.3: {}
|
||||
|
||||
opener@1.5.2: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
|
|
@ -16181,6 +16302,8 @@ snapshots:
|
|||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
radash@12.1.1: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
|
@ -17098,6 +17221,8 @@ snapshots:
|
|||
|
||||
tabbable@6.3.0: {}
|
||||
|
||||
tagged-tag@1.0.0: {}
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2):
|
||||
|
|
@ -17305,9 +17430,13 @@ snapshots:
|
|||
type-fest@4.2.0:
|
||||
optional: true
|
||||
|
||||
type-fest@5.4.0:
|
||||
dependencies:
|
||||
tagged-tag: 1.0.0
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.1: {}
|
||||
ufo@1.6.2: {}
|
||||
|
||||
uglify-js@3.19.3: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ export type IOtherOptions = {
|
|||
needAllResponseContent?: boolean
|
||||
deleteContentType?: boolean
|
||||
silent?: boolean
|
||||
|
||||
/** If true, behaves like standard fetch: no URL prefix, returns raw Response */
|
||||
fetchCompat?: boolean
|
||||
request?: Request
|
||||
|
||||
onData?: IOnData // for stream
|
||||
onThought?: IOnThought
|
||||
onFile?: IOnFile
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { CurrentPlanInfoBackend, SubscriptionUrlsBackend } from '@/app/components/billing/type'
|
||||
import { get, put } from './base'
|
||||
import { get } from './base'
|
||||
|
||||
export const fetchCurrentPlanInfo = () => {
|
||||
return get<CurrentPlanInfoBackend>('/features')
|
||||
|
|
@ -8,17 +8,3 @@ export const fetchCurrentPlanInfo = () => {
|
|||
export const fetchSubscriptionUrls = (plan: string, interval: string) => {
|
||||
return get<SubscriptionUrlsBackend>(`/billing/subscription?plan=${plan}&interval=${interval}`)
|
||||
}
|
||||
|
||||
export const fetchBillingUrl = () => {
|
||||
return get<{ url: string }>('/billing/invoices')
|
||||
}
|
||||
|
||||
export const bindPartnerStackInfo = (partnerKey: string, clickId: string) => {
|
||||
return put(`/billing/partners/${partnerKey}/tenants`, {
|
||||
body: {
|
||||
click_id: clickId,
|
||||
},
|
||||
}, {
|
||||
silent: true,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import type { ContractRouterClient } from '@orpc/contract'
|
||||
import type { JsonifiedClient } from '@orpc/openapi-client'
|
||||
import { createORPCClient, onError } from '@orpc/client'
|
||||
import { OpenAPILink } from '@orpc/openapi-client/fetch'
|
||||
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
||||
import {
|
||||
API_PREFIX,
|
||||
APP_VERSION,
|
||||
IS_MARKETPLACE,
|
||||
MARKETPLACE_API_PREFIX,
|
||||
} from '@/config'
|
||||
import {
|
||||
consoleRouterContract,
|
||||
marketplaceRouterContract,
|
||||
} from '@/contract/router'
|
||||
import { request } from './base'
|
||||
|
||||
const getMarketplaceHeaders = () => new Headers({
|
||||
'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0',
|
||||
})
|
||||
|
||||
const marketplaceLink = new OpenAPILink(marketplaceRouterContract, {
|
||||
url: MARKETPLACE_API_PREFIX,
|
||||
headers: () => (getMarketplaceHeaders()),
|
||||
fetch: (request, init) => {
|
||||
return globalThis.fetch(request, {
|
||||
...init,
|
||||
cache: 'no-store',
|
||||
})
|
||||
},
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
console.error(error)
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
export const marketplaceClient: JsonifiedClient<ContractRouterClient<typeof marketplaceRouterContract>> = createORPCClient(marketplaceLink)
|
||||
export const marketplaceQuery = createTanstackQueryUtils(marketplaceClient, { path: ['marketplace'] })
|
||||
|
||||
const consoleLink = new OpenAPILink(consoleRouterContract, {
|
||||
url: API_PREFIX,
|
||||
fetch: (input, init) => {
|
||||
return request(
|
||||
input.url,
|
||||
init,
|
||||
{
|
||||
fetchCompat: true,
|
||||
request: input,
|
||||
},
|
||||
)
|
||||
},
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
console.error(error)
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
export const consoleClient: JsonifiedClient<ContractRouterClient<typeof consoleRouterContract>> = createORPCClient(consoleLink)
|
||||
export const consoleQuery = createTanstackQueryUtils(consoleClient, { path: ['console'] })
|
||||
|
|
@ -34,7 +34,6 @@ import type {
|
|||
UserProfileOriginResponse,
|
||||
} from '@/models/common'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { del, get, patch, post, put } from './base'
|
||||
|
||||
type LoginSuccess = {
|
||||
|
|
@ -307,10 +306,6 @@ export const fetchSupportRetrievalMethods = (url: string): Promise<RetrievalMeth
|
|||
return get<RetrievalMethodsRes>(url)
|
||||
}
|
||||
|
||||
export const getSystemFeatures = (): Promise<SystemFeatures> => {
|
||||
return get<SystemFeatures>('/system-features')
|
||||
}
|
||||
|
||||
export const enableModel = (url: string, body: { model: string, model_type: ModelTypeEnum }): Promise<CommonResponse> =>
|
||||
patch<CommonResponse>(url, { body })
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
|
|||
needAllResponseContent,
|
||||
deleteContentType,
|
||||
getAbortController,
|
||||
fetchCompat = false,
|
||||
request,
|
||||
} = otherOptions
|
||||
|
||||
let base: string
|
||||
|
|
@ -181,7 +183,7 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
|
|||
},
|
||||
})
|
||||
|
||||
const res = await client(fetchPathname, {
|
||||
const res = await client(request || fetchPathname, {
|
||||
...init,
|
||||
headers,
|
||||
credentials: isMarketplaceAPI
|
||||
|
|
@ -190,8 +192,8 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
|
|||
retry: {
|
||||
methods: [],
|
||||
},
|
||||
...(bodyStringify ? { json: body } : { body: body as BodyInit }),
|
||||
searchParams: params,
|
||||
...(bodyStringify && !fetchCompat ? { json: body } : { body: body as BodyInit }),
|
||||
searchParams: !fetchCompat ? params : undefined,
|
||||
fetch(resource: RequestInfo | URL, options?: RequestInit) {
|
||||
if (resource instanceof Request && options) {
|
||||
const mergedHeaders = new Headers(options.headers || {})
|
||||
|
|
@ -204,7 +206,7 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
|
|||
},
|
||||
})
|
||||
|
||||
if (needAllResponseContent)
|
||||
if (needAllResponseContent || fetchCompat)
|
||||
return res as T
|
||||
const contentType = res.headers.get('content-type')
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { bindPartnerStackInfo, fetchBillingUrl } from '@/service/billing'
|
||||
|
||||
const NAME_SPACE = 'billing'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
|
||||
export const useBindPartnerStackInfo = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'bind-partner-stack'],
|
||||
mutationFn: (data: { partnerKey: string, clickId: string }) => bindPartnerStackInfo(data.partnerKey, data.clickId),
|
||||
mutationKey: consoleQuery.bindPartnerStack.mutationKey(),
|
||||
mutationFn: (data: { partnerKey: string, clickId: string }) => consoleClient.bindPartnerStack({
|
||||
params: { partnerKey: data.partnerKey },
|
||||
body: { click_id: data.clickId },
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useBillingUrl = (enabled: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'url'],
|
||||
queryKey: consoleQuery.billingUrl.queryKey(),
|
||||
enabled,
|
||||
queryFn: async () => {
|
||||
const res = await fetchBillingUrl()
|
||||
const res = await consoleClient.billingUrl()
|
||||
return res.url
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -488,23 +488,23 @@ export const useMutationPluginsFromMarketplace = () => {
|
|||
mutationFn: (pluginsSearchParams: PluginsSearchParams) => {
|
||||
const {
|
||||
query,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category,
|
||||
tags,
|
||||
exclude,
|
||||
type,
|
||||
page = 1,
|
||||
pageSize = 40,
|
||||
page_size = 40,
|
||||
} = pluginsSearchParams
|
||||
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
|
||||
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
|
||||
body: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
page_size,
|
||||
query,
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category: category !== 'all' ? category : '',
|
||||
tags,
|
||||
exclude,
|
||||
|
|
@ -535,23 +535,23 @@ export const useFetchPluginListOrBundleList = (pluginsSearchParams: PluginsSearc
|
|||
queryFn: () => {
|
||||
const {
|
||||
query,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category,
|
||||
tags,
|
||||
exclude,
|
||||
type,
|
||||
page = 1,
|
||||
pageSize = 40,
|
||||
page_size = 40,
|
||||
} = pluginsSearchParams
|
||||
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
|
||||
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
|
||||
body: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
page_size,
|
||||
query,
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
sort_by,
|
||||
sort_order,
|
||||
category: category !== 'all' ? category : '',
|
||||
tags,
|
||||
exclude,
|
||||
|
|
|
|||
Loading…
Reference in New Issue