diff --git a/web/app/(commonLayout)/plugins/page.tsx b/web/app/(commonLayout)/plugins/page.tsx index f44ff6522a..921b129781 100644 --- a/web/app/(commonLayout)/plugins/page.tsx +++ b/web/app/(commonLayout)/plugins/page.tsx @@ -8,7 +8,7 @@ const PluginList = async () => { return ( } - marketplace={} + marketplace={} /> ) } diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index ab3e3e5593..c0d2e86885 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -285,3 +285,25 @@ export const useMarketplace = (providers: ModelProvider[], searchText: string) = plugins: plugins?.filter(plugin => plugin.type !== 'bundle'), } } + +export const useMarketplaceAllPlugins = () => { + const { + plugins, + queryPlugins, + isLoading, + } = useMarketplacePlugins() + + useEffect(() => { + queryPlugins({ + query: '', + category: PluginType.model, + type: 'plugin', + pageSize: 1000, + }) + }, [queryPlugins]) + + return { + plugins: plugins?.filter(plugin => plugin.type !== 'bundle'), + isLoading, + } +} diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 8eea7d3472..35946a73e2 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -22,6 +22,7 @@ import { import { useDefaultModel, useMarketplace, + useMarketplaceAllPlugins, useUpdateModelList, useUpdateModelProviders, } from './hooks' @@ -128,6 +129,10 @@ const ModelProviderPage = ({ searchText }: Props) => { marketplaceCollectionPluginsMap, isLoading: isPluginsLoading, } = useMarketplace(providers, searchText) + const { + plugins: allPlugins, + isLoading: isAllPluginsLoading, + } = useMarketplaceAllPlugins() const cardRender = useCallback((plugin: Plugin) => { if (plugin.type === 'bundle') @@ -206,12 +211,12 @@ const ModelProviderPage = ({ searchText }: Props) => {
{t('common.modelProvider.discoverMore')} - Dify Marketplace + {t('plugin.marketplace.difyMarketplace')}
- {!collapse && isPluginsLoading && } + {!collapse && (isPluginsLoading || isAllPluginsLoading) && } { !isPluginsLoading && ( { /> ) } + { + !isAllPluginsLoading && ( + + ) + } ) diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/loaded.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/loaded.tsx index 195ff43d00..e2de988a74 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/loaded.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/loaded.tsx @@ -1,10 +1,9 @@ 'use client' -import React from 'react' +import React, { useEffect } from 'react' import Button from '@/app/components/base/button' -import { type PluginDeclaration, type PluginType, TaskStatus, type UpdateFromGitHubPayload } from '../../../types' +import { type Plugin, type PluginDeclaration, TaskStatus, type UpdateFromGitHubPayload } from '../../../types' import Card from '../../../card' -import Badge, { BadgeState } from '@/app/components/base/badge/index' import { pluginManifestToCardPluginProps } from '../../utils' import { useTranslation } from 'react-i18next' import { updateFromGitHub } from '@/service/plugins' @@ -12,13 +11,14 @@ import { useInstallPackageFromGitHub } from '@/service/use-plugins' import { RiLoader2Line } from '@remixicon/react' import { usePluginTaskList } from '@/service/use-plugins' import checkTaskStatus from '../../base/check-task-status' +import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' import { parseGitHubUrl } from '../../utils' -import { useCategories } from '../../../hooks' +import Version from '../../base/version' type LoadedProps = { updatePayload: UpdateFromGitHubPayload uniqueIdentifier: string - payload: PluginDeclaration + payload: PluginDeclaration | Plugin repoUrl: string selectedVersion: string selectedPackage: string @@ -41,12 +41,26 @@ const Loaded: React.FC = ({ onFailed, }) => { const { t } = useTranslation() - const { categoriesMap } = useCategories() + const toInstallVersion = payload.version + const pluginId = (payload as Plugin).plugin_id + const { installedInfo, isLoading } = useCheckInstalled({ + pluginIds: [pluginId], + enabled: !!pluginId, + }) + const installedInfoPayload = installedInfo?.[pluginId] + const installedVersion = installedInfoPayload?.installedVersion + const hasInstalled = !!installedVersion + const [isInstalling, setIsInstalling] = React.useState(false) const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub() const { handleRefetch } = usePluginTaskList() const { check } = checkTaskStatus() + useEffect(() => { + if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier) + onInstalled() + }, [hasInstalled]) + const handleInstall = async () => { if (isInstalling) return setIsInstalling(true) @@ -54,8 +68,9 @@ const Loaded: React.FC = ({ try { const { owner, repo } = parseGitHubUrl(repoUrl) let taskId + let isInstalled if (updatePayload) { - const { all_installed: isInstalled, task_id } = await updateFromGitHub( + const { all_installed, task_id } = await updateFromGitHub( `${owner}/${repo}`, selectedVersion, selectedPackage, @@ -64,31 +79,41 @@ const Loaded: React.FC = ({ ) taskId = task_id - - if (isInstalled) { - onInstalled() - return - } - - handleRefetch() + isInstalled = all_installed } else { - const { all_installed: isInstalled, task_id } = await installPackageFromGitHub({ - repoUrl: `${owner}/${repo}`, - selectedVersion, - selectedPackage, - uniqueIdentifier, - }) - - taskId = task_id - - if (isInstalled) { - onInstalled() - return + if (hasInstalled) { + const { + all_installed, + task_id, + } = await updateFromGitHub( + `${owner}/${repo}`, + selectedVersion, + selectedPackage, + installedInfoPayload.uniqueIdentifier, + uniqueIdentifier, + ) + taskId = task_id + isInstalled = all_installed } + else { + const { all_installed, task_id } = await installPackageFromGitHub({ + repoUrl: `${owner}/${repo}`, + selectedVersion, + selectedPackage, + uniqueIdentifier, + }) - handleRefetch() + taskId = task_id + isInstalled = all_installed + } } + if (isInstalled) { + onInstalled() + return + } + + handleRefetch() const { status, error } = await check({ taskId, @@ -120,8 +145,12 @@ const Loaded: React.FC = ({
{payload.version}} + payload={pluginManifestToCardPluginProps(payload as PluginDeclaration)} + titleLeft={!isLoading && } />
@@ -134,7 +163,7 @@ const Loaded: React.FC = ({ variant='primary' className='min-w-[72px] flex space-x-0.5' onClick={handleInstall} - disabled={isInstalling} + disabled={isInstalling || isLoading} > {isInstalling && } {t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)} diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index 1579ab620d..3236e1aefc 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -45,15 +45,19 @@ export type MarketplaceContextValue = { handleFilterPluginTagsChange: (tags: string[]) => void activePluginType: string handleActivePluginTypeChange: (type: string) => void + page: number + handlePageChange: (page: number) => void plugins?: Plugin[] resetPlugins: () => void sort: PluginsSort handleSortChange: (sort: PluginsSort) => void + handleQueryPluginsWhenNoCollection: () => void marketplaceCollectionsFromClient?: MarketplaceCollection[] setMarketplaceCollectionsFromClient: (collections: MarketplaceCollection[]) => void marketplaceCollectionPluginsMapFromClient?: Record setMarketplaceCollectionPluginsMapFromClient: (map: Record) => void isLoading: boolean + isSuccessCollections: boolean } export const MarketplaceContext = createContext({ @@ -65,15 +69,19 @@ export const MarketplaceContext = createContext({ handleFilterPluginTagsChange: () => {}, activePluginType: 'all', handleActivePluginTypeChange: () => {}, + page: 1, + handlePageChange: () => {}, plugins: undefined, resetPlugins: () => {}, sort: DEFAULT_SORT, handleSortChange: () => {}, + handleQueryPluginsWhenNoCollection: () => {}, marketplaceCollectionsFromClient: [], setMarketplaceCollectionsFromClient: () => {}, marketplaceCollectionPluginsMapFromClient: {}, setMarketplaceCollectionPluginsMapFromClient: () => {}, isLoading: false, + isSuccessCollections: false, }) type MarketplaceContextProviderProps = { @@ -108,6 +116,8 @@ export const MarketplaceContextProvider = ({ const filterPluginTagsRef = useRef(filterPluginTags) const [activePluginType, setActivePluginType] = useState(categoryFromSearchParams) const activePluginTypeRef = useRef(activePluginType) + const [page, setPage] = useState(1) + const pageRef = useRef(page) const [sort, setSort] = useState(DEFAULT_SORT) const sortRef = useRef(sort) const { @@ -117,6 +127,7 @@ export const MarketplaceContextProvider = ({ setMarketplaceCollectionPluginsMap: setMarketplaceCollectionPluginsMapFromClient, queryMarketplaceCollectionsAndPlugins, isLoading, + isSuccess: isSuccessCollections, } = useMarketplaceCollectionsAndPlugins() const { plugins, @@ -135,6 +146,7 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, type: getMarketplaceListFilterType(activePluginTypeRef.current), + page: pageRef.current, }) history.pushState({}, '', `/${searchParams?.language ? `?language=${searchParams?.language}` : ''}`) } @@ -152,6 +164,8 @@ export const MarketplaceContextProvider = ({ const handleSearchPluginTextChange = useCallback((text: string) => { setSearchPluginText(text) searchPluginTextRef.current = text + setPage(1) + pageRef.current = 1 if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { queryMarketplaceCollectionsAndPlugins({ @@ -172,12 +186,15 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, exclude, + page: pageRef.current, }) }, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, resetPlugins, exclude]) const handleFilterPluginTagsChange = useCallback((tags: string[]) => { setFilterPluginTags(tags) filterPluginTagsRef.current = tags + setPage(1) + pageRef.current = 1 if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { queryMarketplaceCollectionsAndPlugins({ @@ -199,12 +216,15 @@ export const MarketplaceContextProvider = ({ sortOrder: sortRef.current.sortOrder, exclude, type: getMarketplaceListFilterType(activePluginTypeRef.current), + page: pageRef.current, }) }, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins, exclude]) const handleActivePluginTypeChange = useCallback((type: string) => { setActivePluginType(type) activePluginTypeRef.current = type + setPage(1) + pageRef.current = 1 if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { queryMarketplaceCollectionsAndPlugins({ @@ -226,12 +246,25 @@ export const MarketplaceContextProvider = ({ sortOrder: sortRef.current.sortOrder, exclude, type: getMarketplaceListFilterType(activePluginTypeRef.current), + page: pageRef.current, }) }, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins, exclude]) - const handleSortChange = useCallback((sort: PluginsSort) => { - setSort(sort) - sortRef.current = sort + const handlePageChange = useCallback(() => { + setPage(pageRef.current + 1) + pageRef.current++ + + if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { + queryMarketplaceCollectionsAndPlugins({ + category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, + condition: getMarketplaceListCondition(activePluginTypeRef.current), + exclude, + type: getMarketplaceListFilterType(activePluginTypeRef.current), + }) + resetPlugins() + + return + } queryPlugins({ query: searchPluginTextRef.current, @@ -241,9 +274,43 @@ export const MarketplaceContextProvider = ({ sortOrder: sortRef.current.sortOrder, exclude, type: getMarketplaceListFilterType(activePluginTypeRef.current), + page: pageRef.current, + }) + }, [exclude, queryPlugins, queryMarketplaceCollectionsAndPlugins, resetPlugins]) + + const handleSortChange = useCallback((sort: PluginsSort) => { + setSort(sort) + sortRef.current = sort + setPage(1) + pageRef.current = 1 + + queryPlugins({ + query: searchPluginTextRef.current, + category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, + tags: filterPluginTagsRef.current, + sortBy: sortRef.current.sortBy, + sortOrder: sortRef.current.sortOrder, + exclude, + type: getMarketplaceListFilterType(activePluginTypeRef.current), + page: pageRef.current, }) }, [queryPlugins, exclude]) + const handleQueryPluginsWhenNoCollection = useCallback(() => { + queryPlugins({ + query: searchPluginTextRef.current, + category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, + tags: filterPluginTagsRef.current, + sortBy: sortRef.current.sortBy, + sortOrder: sortRef.current.sortOrder, + exclude, + type: getMarketplaceListFilterType(activePluginTypeRef.current), + page: pageRef.current, + }) + }, [exclude, queryPlugins]) + + // useMarketplaceContainerScroll(handlePageChange) + return ( {children} diff --git a/web/app/components/plugins/marketplace/hooks.ts b/web/app/components/plugins/marketplace/hooks.ts index f58bb9bacd..1d68d9ec70 100644 --- a/web/app/components/plugins/marketplace/hooks.ts +++ b/web/app/components/plugins/marketplace/hooks.ts @@ -1,5 +1,6 @@ import { useCallback, + useEffect, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -23,16 +24,25 @@ import { export const useMarketplaceCollectionsAndPlugins = () => { const [isLoading, setIsLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) const [marketplaceCollections, setMarketplaceCollections] = useState() const [marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap] = useState>() const queryMarketplaceCollectionsAndPlugins = useCallback(async (query?: CollectionsAndPluginsSearchParams) => { - setIsLoading(true) - const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins(query) - setIsLoading(false) - - setMarketplaceCollections(marketplaceCollections) - setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap) + try { + setIsLoading(true) + setIsSuccess(false) + const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins(query) + setIsLoading(false) + setIsSuccess(true) + setMarketplaceCollections(marketplaceCollections) + setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap) + } + // eslint-disable-next-line unused-imports/no-unused-vars + catch (e) { + setIsLoading(false) + setIsSuccess(false) + } }, []) return { @@ -42,6 +52,7 @@ export const useMarketplaceCollectionsAndPlugins = () => { setMarketplaceCollectionPluginsMap, queryMarketplaceCollectionsAndPlugins, isLoading, + isSuccess, } } @@ -67,6 +78,7 @@ export const useMarketplacePlugins = () => { plugins: data?.data?.plugins.map((plugin) => { return getFormattedPlugin(plugin) }), + total: data?.data?.total, resetPlugins: reset, queryPlugins, queryPluginsWithDebounced, @@ -84,3 +96,28 @@ export const useMixedTranslation = (localeFromOuter?: string) => { t, } } + +export const useMarketplaceContainerScroll = (callback: () => void) => { + const container = document.getElementById('marketplace-container') + + const handleScroll = useCallback((e: Event) => { + const target = e.target as HTMLDivElement + const { + scrollTop, + scrollHeight, + clientHeight, + } = target + if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0) + callback() + }, [callback]) + + useEffect(() => { + if (container) + container.addEventListener('scroll', handleScroll) + + return () => { + if (container) + container.removeEventListener('scroll', handleScroll) + } + }, [container, handleScroll]) +} diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 9f8bbb2e76..8549206e06 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -13,12 +13,14 @@ type MarketplaceProps = { showInstallButton?: boolean shouldExclude?: boolean searchParams?: SearchParams + pluginTypeSwitchClassName?: string } const Marketplace = async ({ locale, showInstallButton = true, shouldExclude, searchParams, + pluginTypeSwitchClassName, }: MarketplaceProps) => { let marketplaceCollections: any = [] let marketplaceCollectionPluginsMap = {} @@ -34,7 +36,10 @@ const Marketplace = async ({ - + v.marketplaceCollectionsFromClient) const marketplaceCollectionPluginsMapFromClient = useMarketplaceContext(v => v.marketplaceCollectionPluginsMapFromClient) const isLoading = useMarketplaceContext(v => v.isLoading) + const isSuccessCollections = useMarketplaceContext(v => v.isSuccessCollections) + const handleQueryPluginsWhenNoCollection = useMarketplaceContext(v => v.handleQueryPluginsWhenNoCollection) + + useEffect(() => { + if (!marketplaceCollectionsFromClient?.length && isSuccessCollections) + handleQueryPluginsWhenNoCollection() + }, [handleQueryPluginsWhenNoCollection, marketplaceCollections, marketplaceCollectionsFromClient, isSuccessCollections]) return (
diff --git a/web/app/components/plugins/marketplace/plugin-type-switch.tsx b/web/app/components/plugins/marketplace/plugin-type-switch.tsx index 82758ad87d..796e3a5073 100644 --- a/web/app/components/plugins/marketplace/plugin-type-switch.tsx +++ b/web/app/components/plugins/marketplace/plugin-type-switch.tsx @@ -19,9 +19,11 @@ export const PLUGIN_TYPE_SEARCH_MAP = { } type PluginTypeSwitchProps = { locale?: string + className?: string } const PluginTypeSwitch = ({ locale, + className, }: PluginTypeSwitchProps) => { const { t } = useMixedTranslation(locale) const activePluginType = useMarketplaceContext(s => s.activePluginType) @@ -57,7 +59,8 @@ const PluginTypeSwitch = ({ return (
{ options.map(option => ( diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index f743507ae4..dd59ed6c57 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -312,6 +312,7 @@ export type UninstallPluginResponse = { export type PluginsFromMarketplaceResponse = { plugins: Plugin[] + total: number } export type PluginsFromMarketplaceByInfoResponse = { list: { diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index e787f6ed84..dbc93acaf4 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -297,11 +297,12 @@ export const useMutationPluginsFromMarketplace = () => { tags, exclude, type, + page = 1, } = pluginsSearchParams return postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/search/basic', { body: { - page: 1, - page_size: 10, + page, + page_size: 100, query, sort_by: sortBy, sort_order: sortOrder,