mirror of
https://github.com/langgenius/dify.git
synced 2025-12-21 15:02:26 +00:00
Compare commits
6 Commits
feat/e2e-t
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcc7393b3b | ||
|
|
98caacfd77 | ||
|
|
b91c417d02 | ||
|
|
cbd36e97ff | ||
|
|
e759243c84 | ||
|
|
10d32beeb1 |
@@ -1,39 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
useCallback,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
useMarketplacePlugins,
|
useMarketplacePlugins,
|
||||||
|
useMarketplacePluginsByCollectionId,
|
||||||
} from '@/app/components/plugins/marketplace/hooks'
|
} from '@/app/components/plugins/marketplace/hooks'
|
||||||
import type { Plugin } from '@/app/components/plugins/types'
|
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils'
|
|
||||||
|
|
||||||
export const useMarketplaceAllPlugins = (providers: any[], searchText: string) => {
|
export const useMarketplaceAllPlugins = (providers: any[], searchText: string) => {
|
||||||
const exclude = useMemo(() => {
|
const exclude = useMemo(() => {
|
||||||
return providers.map(provider => provider.plugin_id)
|
return providers.map(provider => provider.plugin_id)
|
||||||
}, [providers])
|
}, [providers])
|
||||||
const [collectionPlugins, setCollectionPlugins] = useState<Plugin[]>([])
|
const {
|
||||||
|
plugins: collectionPlugins = [],
|
||||||
|
isLoading: isCollectionLoading,
|
||||||
|
} = useMarketplacePluginsByCollectionId('__datasource-settings-pinned-datasources')
|
||||||
const {
|
const {
|
||||||
plugins,
|
plugins,
|
||||||
queryPlugins,
|
queryPlugins,
|
||||||
queryPluginsWithDebounced,
|
queryPluginsWithDebounced,
|
||||||
isLoading,
|
isLoading: isPluginsLoading,
|
||||||
} = useMarketplacePlugins()
|
} = useMarketplacePlugins()
|
||||||
|
|
||||||
const getCollectionPlugins = useCallback(async () => {
|
|
||||||
const collectionPlugins = await getMarketplacePluginsByCollectionId('__datasource-settings-pinned-datasources')
|
|
||||||
|
|
||||||
setCollectionPlugins(collectionPlugins)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCollectionPlugins()
|
|
||||||
}, [getCollectionPlugins])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
queryPluginsWithDebounced({
|
queryPluginsWithDebounced({
|
||||||
@@ -75,6 +64,6 @@ export const useMarketplaceAllPlugins = (providers: any[], searchText: string) =
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: allPlugins,
|
plugins: allPlugins,
|
||||||
isLoading,
|
isLoading: isCollectionLoading || isPluginsLoading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,9 @@ import {
|
|||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import {
|
import {
|
||||||
useMarketplacePlugins,
|
useMarketplacePlugins,
|
||||||
|
useMarketplacePluginsByCollectionId,
|
||||||
} from '@/app/components/plugins/marketplace/hooks'
|
} from '@/app/components/plugins/marketplace/hooks'
|
||||||
import type { Plugin } from '@/app/components/plugins/types'
|
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils'
|
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
|
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
|
||||||
@@ -255,25 +254,17 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
|
|||||||
const exclude = useMemo(() => {
|
const exclude = useMemo(() => {
|
||||||
return providers.map(provider => provider.provider.replace(/(.+)\/([^/]+)$/, '$1'))
|
return providers.map(provider => provider.provider.replace(/(.+)\/([^/]+)$/, '$1'))
|
||||||
}, [providers])
|
}, [providers])
|
||||||
const [collectionPlugins, setCollectionPlugins] = useState<Plugin[]>([])
|
const {
|
||||||
|
plugins: collectionPlugins = [],
|
||||||
|
isLoading: isCollectionLoading,
|
||||||
|
} = useMarketplacePluginsByCollectionId('__model-settings-pinned-models')
|
||||||
const {
|
const {
|
||||||
plugins,
|
plugins,
|
||||||
queryPlugins,
|
queryPlugins,
|
||||||
queryPluginsWithDebounced,
|
queryPluginsWithDebounced,
|
||||||
isLoading,
|
isLoading: isPluginsLoading,
|
||||||
} = useMarketplacePlugins()
|
} = useMarketplacePlugins()
|
||||||
|
|
||||||
const getCollectionPlugins = useCallback(async () => {
|
|
||||||
const collectionPlugins = await getMarketplacePluginsByCollectionId('__model-settings-pinned-models')
|
|
||||||
|
|
||||||
setCollectionPlugins(collectionPlugins)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCollectionPlugins()
|
|
||||||
}, [getCollectionPlugins])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
queryPluginsWithDebounced({
|
queryPluginsWithDebounced({
|
||||||
@@ -315,7 +306,7 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: allPlugins,
|
plugins: allPlugins,
|
||||||
isLoading,
|
isLoading: isCollectionLoading || isPluginsLoading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ export const DEFAULT_SORT = {
|
|||||||
sortBy: 'install_count',
|
sortBy: 'install_count',
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SCROLL_BOTTOM_THRESHOLD = 100
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export type MarketplaceContextValue = {
|
|||||||
activePluginType: string
|
activePluginType: string
|
||||||
handleActivePluginTypeChange: (type: string) => void
|
handleActivePluginTypeChange: (type: string) => void
|
||||||
page: number
|
page: number
|
||||||
handlePageChange: (page: number) => void
|
handlePageChange: () => void
|
||||||
plugins?: Plugin[]
|
plugins?: Plugin[]
|
||||||
pluginsTotal?: number
|
pluginsTotal?: number
|
||||||
resetPlugins: () => void
|
resetPlugins: () => void
|
||||||
@@ -128,8 +128,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
const filterPluginTagsRef = useRef(filterPluginTags)
|
const filterPluginTagsRef = useRef(filterPluginTags)
|
||||||
const [activePluginType, setActivePluginType] = useState(categoryFromSearchParams)
|
const [activePluginType, setActivePluginType] = useState(categoryFromSearchParams)
|
||||||
const activePluginTypeRef = useRef(activePluginType)
|
const activePluginTypeRef = useRef(activePluginType)
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const pageRef = useRef(page)
|
|
||||||
const [sort, setSort] = useState(DEFAULT_SORT)
|
const [sort, setSort] = useState(DEFAULT_SORT)
|
||||||
const sortRef = useRef(sort)
|
const sortRef = useRef(sort)
|
||||||
const {
|
const {
|
||||||
@@ -149,7 +147,11 @@ export const MarketplaceContextProvider = ({
|
|||||||
queryPluginsWithDebounced,
|
queryPluginsWithDebounced,
|
||||||
cancelQueryPluginsWithDebounced,
|
cancelQueryPluginsWithDebounced,
|
||||||
isLoading: isPluginsLoading,
|
isLoading: isPluginsLoading,
|
||||||
|
fetchNextPage: fetchNextPluginsPage,
|
||||||
|
hasNextPage: hasNextPluginsPage,
|
||||||
|
page: pluginsPage,
|
||||||
} = useMarketplacePlugins()
|
} = useMarketplacePlugins()
|
||||||
|
const page = Math.max(pluginsPage || 0, 1)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryFromSearchParams || hasValidTags || hasValidCategory) {
|
if (queryFromSearchParams || hasValidTags || hasValidCategory) {
|
||||||
@@ -160,7 +162,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
sortBy: sortRef.current.sortBy,
|
sortBy: sortRef.current.sortBy,
|
||||||
sortOrder: sortRef.current.sortOrder,
|
sortOrder: sortRef.current.sortOrder,
|
||||||
type: getMarketplaceListFilterType(activePluginTypeRef.current),
|
type: getMarketplaceListFilterType(activePluginTypeRef.current),
|
||||||
page: pageRef.current,
|
|
||||||
})
|
})
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
if (searchParams?.language)
|
if (searchParams?.language)
|
||||||
@@ -221,7 +222,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
sortOrder: sortRef.current.sortOrder,
|
sortOrder: sortRef.current.sortOrder,
|
||||||
exclude,
|
exclude,
|
||||||
type: getMarketplaceListFilterType(activePluginTypeRef.current),
|
type: getMarketplaceListFilterType(activePluginTypeRef.current),
|
||||||
page: pageRef.current,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -233,7 +233,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
sortOrder: sortRef.current.sortOrder,
|
sortOrder: sortRef.current.sortOrder,
|
||||||
exclude,
|
exclude,
|
||||||
type: getMarketplaceListFilterType(activePluginTypeRef.current),
|
type: getMarketplaceListFilterType(activePluginTypeRef.current),
|
||||||
page: pageRef.current,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [exclude, queryPluginsWithDebounced, queryPlugins, handleUpdateSearchParams])
|
}, [exclude, queryPluginsWithDebounced, queryPlugins, handleUpdateSearchParams])
|
||||||
@@ -252,8 +251,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
const handleSearchPluginTextChange = useCallback((text: string) => {
|
const handleSearchPluginTextChange = useCallback((text: string) => {
|
||||||
setSearchPluginText(text)
|
setSearchPluginText(text)
|
||||||
searchPluginTextRef.current = text
|
searchPluginTextRef.current = text
|
||||||
setPage(1)
|
|
||||||
pageRef.current = 1
|
|
||||||
|
|
||||||
handleQuery(true)
|
handleQuery(true)
|
||||||
}, [handleQuery])
|
}, [handleQuery])
|
||||||
@@ -261,8 +258,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
const handleFilterPluginTagsChange = useCallback((tags: string[]) => {
|
const handleFilterPluginTagsChange = useCallback((tags: string[]) => {
|
||||||
setFilterPluginTags(tags)
|
setFilterPluginTags(tags)
|
||||||
filterPluginTagsRef.current = tags
|
filterPluginTagsRef.current = tags
|
||||||
setPage(1)
|
|
||||||
pageRef.current = 1
|
|
||||||
|
|
||||||
handleQuery()
|
handleQuery()
|
||||||
}, [handleQuery])
|
}, [handleQuery])
|
||||||
@@ -270,8 +265,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
const handleActivePluginTypeChange = useCallback((type: string) => {
|
const handleActivePluginTypeChange = useCallback((type: string) => {
|
||||||
setActivePluginType(type)
|
setActivePluginType(type)
|
||||||
activePluginTypeRef.current = type
|
activePluginTypeRef.current = type
|
||||||
setPage(1)
|
|
||||||
pageRef.current = 1
|
|
||||||
|
|
||||||
handleQuery()
|
handleQuery()
|
||||||
}, [handleQuery])
|
}, [handleQuery])
|
||||||
@@ -279,20 +272,14 @@ export const MarketplaceContextProvider = ({
|
|||||||
const handleSortChange = useCallback((sort: PluginsSort) => {
|
const handleSortChange = useCallback((sort: PluginsSort) => {
|
||||||
setSort(sort)
|
setSort(sort)
|
||||||
sortRef.current = sort
|
sortRef.current = sort
|
||||||
setPage(1)
|
|
||||||
pageRef.current = 1
|
|
||||||
|
|
||||||
handleQueryPlugins()
|
handleQueryPlugins()
|
||||||
}, [handleQueryPlugins])
|
}, [handleQueryPlugins])
|
||||||
|
|
||||||
const handlePageChange = useCallback(() => {
|
const handlePageChange = useCallback(() => {
|
||||||
if (pluginsTotal && plugins && pluginsTotal > plugins.length) {
|
if (hasNextPluginsPage)
|
||||||
setPage(pageRef.current + 1)
|
fetchNextPluginsPage()
|
||||||
pageRef.current++
|
}, [fetchNextPluginsPage, hasNextPluginsPage])
|
||||||
|
|
||||||
handleQueryPlugins()
|
|
||||||
}
|
|
||||||
}, [handleQueryPlugins, plugins, pluginsTotal])
|
|
||||||
|
|
||||||
const handleMoreClick = useCallback((searchParams: SearchParamsFromCollection) => {
|
const handleMoreClick = useCallback((searchParams: SearchParamsFromCollection) => {
|
||||||
setSearchPluginText(searchParams?.query || '')
|
setSearchPluginText(searchParams?.query || '')
|
||||||
@@ -305,9 +292,6 @@ export const MarketplaceContextProvider = ({
|
|||||||
sortBy: searchParams?.sort_by || DEFAULT_SORT.sortBy,
|
sortBy: searchParams?.sort_by || DEFAULT_SORT.sortBy,
|
||||||
sortOrder: searchParams?.sort_order || DEFAULT_SORT.sortOrder,
|
sortOrder: searchParams?.sort_order || DEFAULT_SORT.sortOrder,
|
||||||
}
|
}
|
||||||
setPage(1)
|
|
||||||
pageRef.current = 1
|
|
||||||
|
|
||||||
handleQueryPlugins()
|
handleQueryPlugins()
|
||||||
}, [handleQueryPlugins])
|
}, [handleQueryPlugins])
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import {
|
||||||
|
useInfiniteQuery,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
import type {
|
import type {
|
||||||
@@ -16,39 +21,41 @@ import type {
|
|||||||
import {
|
import {
|
||||||
getFormattedPlugin,
|
getFormattedPlugin,
|
||||||
getMarketplaceCollectionsAndPlugins,
|
getMarketplaceCollectionsAndPlugins,
|
||||||
|
getMarketplacePluginsByCollectionId,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
import { SCROLL_BOTTOM_THRESHOLD } from './constants'
|
||||||
import i18n from '@/i18n-config/i18next-config'
|
import i18n from '@/i18n-config/i18next-config'
|
||||||
import {
|
import { postMarketplace } from '@/service/base'
|
||||||
useMutationPluginsFromMarketplace,
|
import type { PluginsFromMarketplaceResponse } from '@/app/components/plugins/types'
|
||||||
} from '@/service/use-plugins'
|
|
||||||
|
|
||||||
export const useMarketplaceCollectionsAndPlugins = () => {
|
export const useMarketplaceCollectionsAndPlugins = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [queryParams, setQueryParams] = useState<CollectionsAndPluginsSearchParams>()
|
||||||
const [isSuccess, setIsSuccess] = useState(false)
|
const [marketplaceCollectionsOverride, setMarketplaceCollections] = useState<MarketplaceCollection[]>()
|
||||||
const [marketplaceCollections, setMarketplaceCollections] = useState<MarketplaceCollection[]>()
|
const [marketplaceCollectionPluginsMapOverride, setMarketplaceCollectionPluginsMap] = useState<Record<string, Plugin[]>>()
|
||||||
const [marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap] = useState<Record<string, Plugin[]>>()
|
|
||||||
|
|
||||||
const queryMarketplaceCollectionsAndPlugins = useCallback(async (query?: CollectionsAndPluginsSearchParams) => {
|
const {
|
||||||
try {
|
data,
|
||||||
setIsLoading(true)
|
isFetching,
|
||||||
setIsSuccess(false)
|
isSuccess,
|
||||||
const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins(query)
|
isPending,
|
||||||
setIsLoading(false)
|
} = useQuery({
|
||||||
setIsSuccess(true)
|
queryKey: ['marketplaceCollectionsAndPlugins', queryParams],
|
||||||
setMarketplaceCollections(marketplaceCollections)
|
queryFn: ({ signal }) => getMarketplaceCollectionsAndPlugins(queryParams, { signal }),
|
||||||
setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap)
|
enabled: queryParams !== undefined,
|
||||||
}
|
staleTime: 1000 * 60 * 5,
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
gcTime: 1000 * 60 * 10,
|
||||||
catch (e) {
|
retry: false,
|
||||||
setIsLoading(false)
|
})
|
||||||
setIsSuccess(false)
|
|
||||||
}
|
const queryMarketplaceCollectionsAndPlugins = useCallback((query?: CollectionsAndPluginsSearchParams) => {
|
||||||
|
setQueryParams(query ? { ...query } : {})
|
||||||
}, [])
|
}, [])
|
||||||
|
const isLoading = !!queryParams && (isFetching || isPending)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marketplaceCollections,
|
marketplaceCollections: marketplaceCollectionsOverride ?? data?.marketplaceCollections,
|
||||||
setMarketplaceCollections,
|
setMarketplaceCollections,
|
||||||
marketplaceCollectionPluginsMap,
|
marketplaceCollectionPluginsMap: marketplaceCollectionPluginsMapOverride ?? data?.marketplaceCollectionPluginsMap,
|
||||||
setMarketplaceCollectionPluginsMap,
|
setMarketplaceCollectionPluginsMap,
|
||||||
queryMarketplaceCollectionsAndPlugins,
|
queryMarketplaceCollectionsAndPlugins,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -56,37 +63,128 @@ export const useMarketplaceCollectionsAndPlugins = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMarketplacePlugins = () => {
|
export const useMarketplacePluginsByCollectionId = (
|
||||||
|
collectionId?: string,
|
||||||
|
query?: CollectionsAndPluginsSearchParams,
|
||||||
|
) => {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
mutateAsync,
|
isFetching,
|
||||||
reset,
|
isSuccess,
|
||||||
isPending,
|
isPending,
|
||||||
} = useMutationPluginsFromMarketplace()
|
} = useQuery({
|
||||||
|
queryKey: ['marketplaceCollectionPlugins', collectionId, query],
|
||||||
|
queryFn: ({ signal }) => {
|
||||||
|
if (!collectionId)
|
||||||
|
return Promise.resolve<Plugin[]>([])
|
||||||
|
return getMarketplacePluginsByCollectionId(collectionId, query, { signal })
|
||||||
|
},
|
||||||
|
enabled: !!collectionId,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
gcTime: 1000 * 60 * 10,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
const [prevPlugins, setPrevPlugins] = useState<Plugin[] | undefined>()
|
return {
|
||||||
|
plugins: data || [],
|
||||||
|
isLoading: !!collectionId && (isFetching || isPending),
|
||||||
|
isSuccess,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMarketplacePlugins = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [queryParams, setQueryParams] = useState<PluginsSearchParams>()
|
||||||
|
|
||||||
|
const normalizeParams = useCallback((pluginsSearchParams: PluginsSearchParams) => {
|
||||||
|
const pageSize = pluginsSearchParams.pageSize || 40
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pluginsSearchParams,
|
||||||
|
pageSize,
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const marketplacePluginsQuery = useInfiniteQuery({
|
||||||
|
queryKey: ['marketplacePlugins', queryParams],
|
||||||
|
queryFn: async ({ pageParam = 1, signal }) => {
|
||||||
|
if (!queryParams) {
|
||||||
|
return {
|
||||||
|
plugins: [] as Plugin[],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 40,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = normalizeParams(queryParams)
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
category,
|
||||||
|
tags,
|
||||||
|
exclude,
|
||||||
|
type,
|
||||||
|
pageSize,
|
||||||
|
} = params
|
||||||
|
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
|
||||||
|
body: {
|
||||||
|
page: pageParam,
|
||||||
|
page_size: pageSize,
|
||||||
|
query,
|
||||||
|
sort_by: sortBy,
|
||||||
|
sort_order: sortOrder,
|
||||||
|
category: category !== 'all' ? category : '',
|
||||||
|
tags,
|
||||||
|
exclude,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
const resPlugins = res.data.bundles || res.data.plugins || []
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: resPlugins.map(plugin => getFormattedPlugin(plugin)),
|
||||||
|
total: res.data.total,
|
||||||
|
page: pageParam,
|
||||||
|
pageSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return {
|
||||||
|
plugins: [],
|
||||||
|
total: 0,
|
||||||
|
page: pageParam,
|
||||||
|
pageSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const nextPage = lastPage.page + 1
|
||||||
|
const loaded = lastPage.page * lastPage.pageSize
|
||||||
|
return loaded < (lastPage.total || 0) ? nextPage : undefined
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
enabled: !!queryParams,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
gcTime: 1000 * 60 * 10,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
const resetPlugins = useCallback(() => {
|
const resetPlugins = useCallback(() => {
|
||||||
reset()
|
setQueryParams(undefined)
|
||||||
setPrevPlugins(undefined)
|
queryClient.removeQueries({
|
||||||
}, [reset])
|
queryKey: ['marketplacePlugins'],
|
||||||
|
})
|
||||||
|
}, [queryClient])
|
||||||
|
|
||||||
const handleUpdatePlugins = useCallback((pluginsSearchParams: PluginsSearchParams) => {
|
const handleUpdatePlugins = useCallback((pluginsSearchParams: PluginsSearchParams) => {
|
||||||
mutateAsync(pluginsSearchParams).then((res) => {
|
setQueryParams(normalizeParams(pluginsSearchParams))
|
||||||
const currentPage = pluginsSearchParams.page || 1
|
}, [normalizeParams])
|
||||||
const resPlugins = res.data.bundles || res.data.plugins
|
|
||||||
if (currentPage > 1) {
|
|
||||||
setPrevPlugins(prevPlugins => [...(prevPlugins || []), ...resPlugins.map((plugin) => {
|
|
||||||
return getFormattedPlugin(plugin)
|
|
||||||
})])
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setPrevPlugins(resPlugins.map((plugin) => {
|
|
||||||
return getFormattedPlugin(plugin)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [mutateAsync])
|
|
||||||
|
|
||||||
const { run: queryPluginsWithDebounced, cancel: cancelQueryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams: PluginsSearchParams) => {
|
const { run: queryPluginsWithDebounced, cancel: cancelQueryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams: PluginsSearchParams) => {
|
||||||
handleUpdatePlugins(pluginsSearchParams)
|
handleUpdatePlugins(pluginsSearchParams)
|
||||||
@@ -94,14 +192,29 @@ export const useMarketplacePlugins = () => {
|
|||||||
wait: 500,
|
wait: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasQuery = !!queryParams
|
||||||
|
const hasData = marketplacePluginsQuery.data !== undefined
|
||||||
|
const plugins = hasQuery && hasData
|
||||||
|
? marketplacePluginsQuery.data.pages.flatMap(page => page.plugins)
|
||||||
|
: undefined
|
||||||
|
const total = hasQuery && hasData ? marketplacePluginsQuery.data.pages?.[0]?.total : undefined
|
||||||
|
const isPluginsLoading = hasQuery && (
|
||||||
|
marketplacePluginsQuery.isPending
|
||||||
|
|| (marketplacePluginsQuery.isFetching && !marketplacePluginsQuery.data)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: prevPlugins,
|
plugins,
|
||||||
total: data?.data?.total,
|
total,
|
||||||
resetPlugins,
|
resetPlugins,
|
||||||
queryPlugins: handleUpdatePlugins,
|
queryPlugins: handleUpdatePlugins,
|
||||||
queryPluginsWithDebounced,
|
queryPluginsWithDebounced,
|
||||||
cancelQueryPluginsWithDebounced,
|
cancelQueryPluginsWithDebounced,
|
||||||
isLoading: isPending,
|
isLoading: isPluginsLoading,
|
||||||
|
isFetchingNextPage: marketplacePluginsQuery.isFetchingNextPage,
|
||||||
|
hasNextPage: marketplacePluginsQuery.hasNextPage,
|
||||||
|
fetchNextPage: marketplacePluginsQuery.fetchNextPage,
|
||||||
|
page: marketplacePluginsQuery.data?.pages?.length || (marketplacePluginsQuery.isPending && hasQuery ? 1 : 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +244,7 @@ export const useMarketplaceContainerScroll = (
|
|||||||
scrollHeight,
|
scrollHeight,
|
||||||
clientHeight,
|
clientHeight,
|
||||||
} = target
|
} = target
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0)
|
if (scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_THRESHOLD && scrollTop > 0)
|
||||||
callback()
|
callback()
|
||||||
}, [callback])
|
}, [callback])
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import IntersectionLine from './intersection-line'
|
|||||||
import SearchBoxWrapper from './search-box/search-box-wrapper'
|
import SearchBoxWrapper from './search-box/search-box-wrapper'
|
||||||
import PluginTypeSwitch from './plugin-type-switch'
|
import PluginTypeSwitch from './plugin-type-switch'
|
||||||
import ListWrapper from './list/list-wrapper'
|
import ListWrapper from './list/list-wrapper'
|
||||||
import type { SearchParams } from './types'
|
import type { MarketplaceCollection, SearchParams } from './types'
|
||||||
|
import type { Plugin } from '@/app/components/plugins/types'
|
||||||
import { getMarketplaceCollectionsAndPlugins } from './utils'
|
import { getMarketplaceCollectionsAndPlugins } from './utils'
|
||||||
import { TanstackQueryInitializer } from '@/context/query-client'
|
import { TanstackQueryInitializer } from '@/context/query-client'
|
||||||
|
|
||||||
@@ -30,8 +31,8 @@ const Marketplace = async ({
|
|||||||
scrollContainerId,
|
scrollContainerId,
|
||||||
showSearchParams = true,
|
showSearchParams = true,
|
||||||
}: MarketplaceProps) => {
|
}: MarketplaceProps) => {
|
||||||
let marketplaceCollections: any = []
|
let marketplaceCollections: MarketplaceCollection[] = []
|
||||||
let marketplaceCollectionPluginsMap = {}
|
let marketplaceCollectionPluginsMap: Record<string, Plugin[]> = {}
|
||||||
if (!shouldExclude) {
|
if (!shouldExclude) {
|
||||||
const marketplaceCollectionsAndPluginsData = await getMarketplaceCollectionsAndPlugins()
|
const marketplaceCollectionsAndPluginsData = await getMarketplaceCollectionsAndPlugins()
|
||||||
marketplaceCollections = marketplaceCollectionsAndPluginsData.marketplaceCollections
|
marketplaceCollections = marketplaceCollectionsAndPluginsData.marketplaceCollections
|
||||||
|
|||||||
@@ -28,13 +28,20 @@ const ListWrapper = ({
|
|||||||
const isLoading = useMarketplaceContext(v => v.isLoading)
|
const isLoading = useMarketplaceContext(v => v.isLoading)
|
||||||
const isSuccessCollections = useMarketplaceContext(v => v.isSuccessCollections)
|
const isSuccessCollections = useMarketplaceContext(v => v.isSuccessCollections)
|
||||||
const handleQueryPlugins = useMarketplaceContext(v => v.handleQueryPlugins)
|
const handleQueryPlugins = useMarketplaceContext(v => v.handleQueryPlugins)
|
||||||
|
const searchPluginText = useMarketplaceContext(v => v.searchPluginText)
|
||||||
|
const filterPluginTags = useMarketplaceContext(v => v.filterPluginTags)
|
||||||
const page = useMarketplaceContext(v => v.page)
|
const page = useMarketplaceContext(v => v.page)
|
||||||
const handleMoreClick = useMarketplaceContext(v => v.handleMoreClick)
|
const handleMoreClick = useMarketplaceContext(v => v.handleMoreClick)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!marketplaceCollectionsFromClient?.length && isSuccessCollections)
|
if (
|
||||||
|
!marketplaceCollectionsFromClient?.length
|
||||||
|
&& isSuccessCollections
|
||||||
|
&& !searchPluginText
|
||||||
|
&& !filterPluginTags.length
|
||||||
|
)
|
||||||
handleQueryPlugins()
|
handleQueryPlugins()
|
||||||
}, [handleQueryPlugins, marketplaceCollections, marketplaceCollectionsFromClient, isSuccessCollections])
|
}, [handleQueryPlugins, marketplaceCollections, marketplaceCollectionsFromClient, isSuccessCollections, searchPluginText, filterPluginTags])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ import {
|
|||||||
} from '@/config'
|
} from '@/config'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
|
type MarketplaceFetchOptions = {
|
||||||
|
signal?: AbortSignal
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMarketplaceHeaders = () => new Headers({
|
||||||
|
'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0',
|
||||||
|
})
|
||||||
|
|
||||||
export const getPluginIconInMarketplace = (plugin: Plugin) => {
|
export const getPluginIconInMarketplace = (plugin: Plugin) => {
|
||||||
if (plugin.type === 'bundle')
|
if (plugin.type === 'bundle')
|
||||||
return `${MARKETPLACE_API_PREFIX}/bundles/${plugin.org}/${plugin.name}/icon`
|
return `${MARKETPLACE_API_PREFIX}/bundles/${plugin.org}/${plugin.name}/icon`
|
||||||
@@ -46,20 +54,23 @@ export const getPluginDetailLinkInMarketplace = (plugin: Plugin) => {
|
|||||||
return `/plugins/${plugin.org}/${plugin.name}`
|
return `/plugins/${plugin.org}/${plugin.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => {
|
export const getMarketplacePluginsByCollectionId = async (
|
||||||
let plugins: Plugin[]
|
collectionId: string,
|
||||||
|
query?: CollectionsAndPluginsSearchParams,
|
||||||
|
options?: MarketplaceFetchOptions,
|
||||||
|
) => {
|
||||||
|
let plugins: Plugin[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `${MARKETPLACE_API_PREFIX}/collections/${collectionId}/plugins`
|
const url = `${MARKETPLACE_API_PREFIX}/collections/${collectionId}/plugins`
|
||||||
const headers = new Headers({
|
const headers = getMarketplaceHeaders()
|
||||||
'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0',
|
|
||||||
})
|
|
||||||
const marketplaceCollectionPluginsData = await globalThis.fetch(
|
const marketplaceCollectionPluginsData = await globalThis.fetch(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
|
signal: options?.signal,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
category: query?.category,
|
category: query?.category,
|
||||||
exclude: query?.exclude,
|
exclude: query?.exclude,
|
||||||
@@ -68,9 +79,7 @@ export const getMarketplacePluginsByCollectionId = async (collectionId: string,
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
const marketplaceCollectionPluginsDataJson = await marketplaceCollectionPluginsData.json()
|
const marketplaceCollectionPluginsDataJson = await marketplaceCollectionPluginsData.json()
|
||||||
plugins = marketplaceCollectionPluginsDataJson.data.plugins.map((plugin: Plugin) => {
|
plugins = (marketplaceCollectionPluginsDataJson.data.plugins || []).map((plugin: Plugin) => getFormattedPlugin(plugin))
|
||||||
return getFormattedPlugin(plugin)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@@ -80,23 +89,31 @@ export const getMarketplacePluginsByCollectionId = async (collectionId: string,
|
|||||||
return plugins
|
return plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMarketplaceCollectionsAndPlugins = async (query?: CollectionsAndPluginsSearchParams) => {
|
export const getMarketplaceCollectionsAndPlugins = async (
|
||||||
let marketplaceCollections = [] as MarketplaceCollection[]
|
query?: CollectionsAndPluginsSearchParams,
|
||||||
let marketplaceCollectionPluginsMap = {} as Record<string, Plugin[]>
|
options?: MarketplaceFetchOptions,
|
||||||
|
) => {
|
||||||
|
let marketplaceCollections: MarketplaceCollection[] = []
|
||||||
|
let marketplaceCollectionPluginsMap: Record<string, Plugin[]> = {}
|
||||||
try {
|
try {
|
||||||
let marketplaceUrl = `${MARKETPLACE_API_PREFIX}/collections?page=1&page_size=100`
|
let marketplaceUrl = `${MARKETPLACE_API_PREFIX}/collections?page=1&page_size=100`
|
||||||
if (query?.condition)
|
if (query?.condition)
|
||||||
marketplaceUrl += `&condition=${query.condition}`
|
marketplaceUrl += `&condition=${query.condition}`
|
||||||
if (query?.type)
|
if (query?.type)
|
||||||
marketplaceUrl += `&type=${query.type}`
|
marketplaceUrl += `&type=${query.type}`
|
||||||
const headers = new Headers({
|
const headers = getMarketplaceHeaders()
|
||||||
'X-Dify-Version': !IS_MARKETPLACE ? APP_VERSION : '999.0.0',
|
const marketplaceCollectionsData = await globalThis.fetch(
|
||||||
})
|
marketplaceUrl,
|
||||||
const marketplaceCollectionsData = await globalThis.fetch(marketplaceUrl, { headers, cache: 'no-store' })
|
{
|
||||||
|
headers,
|
||||||
|
cache: 'no-store',
|
||||||
|
signal: options?.signal,
|
||||||
|
},
|
||||||
|
)
|
||||||
const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json()
|
const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json()
|
||||||
marketplaceCollections = marketplaceCollectionsDataJson.data.collections
|
marketplaceCollections = marketplaceCollectionsDataJson.data.collections || []
|
||||||
await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => {
|
await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => {
|
||||||
const plugins = await getMarketplacePluginsByCollectionId(collection.name, query)
|
const plugins = await getMarketplacePluginsByCollectionId(collection.name, query, options)
|
||||||
|
|
||||||
marketplaceCollectionPluginsMap[collection.name] = plugins
|
marketplaceCollectionPluginsMap[collection.name] = plugins
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
useMarketplaceCollectionsAndPlugins,
|
useMarketplaceCollectionsAndPlugins,
|
||||||
useMarketplacePlugins,
|
useMarketplacePlugins,
|
||||||
} from '@/app/components/plugins/marketplace/hooks'
|
} from '@/app/components/plugins/marketplace/hooks'
|
||||||
|
import { SCROLL_BOTTOM_THRESHOLD } from '@/app/components/plugins/marketplace/constants'
|
||||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||||
import { getMarketplaceListCondition } from '@/app/components/plugins/marketplace/utils'
|
import { getMarketplaceListCondition } from '@/app/components/plugins/marketplace/utils'
|
||||||
import { useAllToolProviders } from '@/service/use-tools'
|
import { useAllToolProviders } from '@/service/use-tools'
|
||||||
@@ -31,10 +31,10 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
|
|||||||
queryPlugins,
|
queryPlugins,
|
||||||
queryPluginsWithDebounced,
|
queryPluginsWithDebounced,
|
||||||
isLoading: isPluginsLoading,
|
isLoading: isPluginsLoading,
|
||||||
total: pluginsTotal,
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
page: pluginsPage,
|
||||||
} = useMarketplacePlugins()
|
} = useMarketplacePlugins()
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const pageRef = useRef(page)
|
|
||||||
const searchPluginTextRef = useRef(searchPluginText)
|
const searchPluginTextRef = useRef(searchPluginText)
|
||||||
const filterPluginTagsRef = useRef(filterPluginTags)
|
const filterPluginTagsRef = useRef(filterPluginTags)
|
||||||
|
|
||||||
@@ -44,9 +44,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
|
|||||||
}, [searchPluginText, filterPluginTags])
|
}, [searchPluginText, filterPluginTags])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((searchPluginText || filterPluginTags.length) && isSuccess) {
|
if ((searchPluginText || filterPluginTags.length) && isSuccess) {
|
||||||
setPage(1)
|
|
||||||
pageRef.current = 1
|
|
||||||
|
|
||||||
if (searchPluginText) {
|
if (searchPluginText) {
|
||||||
queryPluginsWithDebounced({
|
queryPluginsWithDebounced({
|
||||||
category: PluginCategoryEnum.tool,
|
category: PluginCategoryEnum.tool,
|
||||||
@@ -54,7 +51,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
|
|||||||
tags: filterPluginTags,
|
tags: filterPluginTags,
|
||||||
exclude,
|
exclude,
|
||||||
type: 'plugin',
|
type: 'plugin',
|
||||||
page: pageRef.current,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -64,7 +60,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
|
|||||||
tags: filterPluginTags,
|
tags: filterPluginTags,
|
||||||
exclude,
|
exclude,
|
||||||
type: 'plugin',
|
type: 'plugin',
|
||||||
page: pageRef.current,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -87,24 +82,13 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
|
|||||||
scrollHeight,
|
scrollHeight,
|
||||||
clientHeight,
|
clientHeight,
|
||||||
} = target
|
} = target
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0) {
|
if (scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_THRESHOLD && scrollTop > 0) {
|
||||||
const searchPluginText = searchPluginTextRef.current
|
const searchPluginText = searchPluginTextRef.current
|
||||||
const filterPluginTags = filterPluginTagsRef.current
|
const filterPluginTags = filterPluginTagsRef.current
|
||||||
if (pluginsTotal && plugins && pluginsTotal > plugins.length && (!!searchPluginText || !!filterPluginTags.length)) {
|
if (hasNextPage && (!!searchPluginText || !!filterPluginTags.length))
|
||||||
setPage(pageRef.current + 1)
|
fetchNextPage()
|
||||||
pageRef.current++
|
|
||||||
|
|
||||||
queryPlugins({
|
|
||||||
category: PluginCategoryEnum.tool,
|
|
||||||
query: searchPluginText,
|
|
||||||
tags: filterPluginTags,
|
|
||||||
exclude,
|
|
||||||
type: 'plugin',
|
|
||||||
page: pageRef.current,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [exclude, plugins, pluginsTotal, queryPlugins])
|
}, [exclude, fetchNextPage, hasNextPage, plugins, queryPlugins])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: isLoading || isPluginsLoading,
|
isLoading: isLoading || isPluginsLoading,
|
||||||
@@ -112,6 +96,6 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
|
|||||||
marketplaceCollectionPluginsMap,
|
marketplaceCollectionPluginsMap,
|
||||||
plugins,
|
plugins,
|
||||||
handleScroll,
|
handleScroll,
|
||||||
page,
|
page: Math.max(pluginsPage || 0, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ export type ConversationListResponse = {
|
|||||||
logs: Conversation[]
|
logs: Conversation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchLogs = (url: string) =>
|
|
||||||
fetch(url).then<ConversationListResponse>(r => r.json())
|
|
||||||
|
|
||||||
export const CompletionParams = ['temperature', 'top_p', 'presence_penalty', 'max_token', 'stop', 'frequency_penalty'] as const
|
export const CompletionParams = ['temperature', 'top_p', 'presence_penalty', 'max_token', 'stop', 'frequency_penalty'] as const
|
||||||
|
|
||||||
export type CompletionParamType = typeof CompletionParams[number]
|
export type CompletionParamType = typeof CompletionParams[number]
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
export type User = {
|
|
||||||
id: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
name: string
|
|
||||||
phone: string
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
avatar: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UserResponse = {
|
|
||||||
users: User[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchUsers = (url: string) =>
|
|
||||||
fetch(url).then<UserResponse>(r => r.json())
|
|
||||||
Reference in New Issue
Block a user