diff --git a/web/app/components/plugins/install-plugin/base/version.tsx b/web/app/components/plugins/install-plugin/base/version.tsx index 891294f095..67bbc8ed2e 100644 --- a/web/app/components/plugins/install-plugin/base/version.tsx +++ b/web/app/components/plugins/install-plugin/base/version.tsx @@ -2,14 +2,9 @@ import type { FC } from 'react' import React from 'react' import Badge, { BadgeState } from '@/app/components/base/badge/index' +import type { VersionProps } from '../../types' -type Props = { - hasInstalled: boolean - installedVersion?: string - toInstallVersion: string -} - -const Version: FC = ({ +const Version: FC = ({ hasInstalled, installedVersion, toInstallVersion, diff --git a/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx b/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx index 5b35128049..e72648fcec 100644 --- a/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx +++ b/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx @@ -1,6 +1,7 @@ import { useCheckInstalled as useDoCheckInstalled } from '@/service/use-plugins' import { useMemo } from 'react' +import type { VersionInfo } from '../../types' type Props = { pluginIds: string[], enabled: boolean @@ -12,10 +13,7 @@ const useCheckInstalled = (props: Props) => { if (!data) return undefined - const res: Record = {} + const res: Record = {} data?.plugins.forEach((plugin) => { res[plugin.plugin_id] = { installedVersion: plugin.declaration.version, diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx index 8440b488b2..96abaa2e1c 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx @@ -6,11 +6,13 @@ import { pluginManifestToCardPluginProps } from '../../utils' import { useUploadGitHub } from '@/service/use-plugins' import Loading from '../../base/loading' import LoadedItem from './loaded-item' +import type { VersionProps } from '@/app/components/plugins/types' type Props = { checked: boolean onCheckedChange: (plugin: Plugin) => void dependency: GitHubItemAndMarketPlaceDependency + versionInfo: VersionProps onFetchedPayload: (payload: Plugin) => void onFetchError: () => void } @@ -19,6 +21,7 @@ const Item: FC = ({ checked, onCheckedChange, dependency, + versionInfo, onFetchedPayload, onFetchError, }) => { @@ -50,6 +53,7 @@ const Item: FC = ({ return ( diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx index 9b503245ce..5eb4c94abe 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx @@ -4,15 +4,17 @@ import React from 'react' import type { Plugin } from '../../../types' import Card from '../../../card' import Checkbox from '@/app/components/base/checkbox' -import Badge, { BadgeState } from '@/app/components/base/badge/index' import useGetIcon from '../../base/use-get-icon' import { MARKETPLACE_API_PREFIX } from '@/config' +import Version from '../../base/version' +import type { VersionProps } from '../../../types' type Props = { checked: boolean onCheckedChange: (plugin: Plugin) => void payload: Plugin isFromMarketPlace?: boolean + versionInfo: VersionProps } const LoadedItem: FC = ({ @@ -20,8 +22,13 @@ const LoadedItem: FC = ({ onCheckedChange, payload, isFromMarketPlace, + versionInfo: particleVersionInfo, }) => { const { getIconUrl } = useGetIcon() + const versionInfo = { + ...particleVersionInfo, + toInstallVersion: payload.version, + } return (
= ({ ...payload, icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon), }} - titleLeft={payload.version ? {payload.version} : null} + titleLeft={payload.version ? : null} />
) diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx index f7de4d09bc..3389bdb0ad 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx @@ -4,25 +4,31 @@ import React from 'react' import type { Plugin } from '../../../types' import Loading from '../../base/loading' import LoadedItem from './loaded-item' +import type { VersionProps } from '@/app/components/plugins/types' type Props = { checked: boolean onCheckedChange: (plugin: Plugin) => void payload?: Plugin + version: string + versionInfo: VersionProps } const MarketPlaceItem: FC = ({ checked, onCheckedChange, payload, + version, + versionInfo, }) => { if (!payload) return return ( ) } diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx index 97ee6b0de6..101c8facaf 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx @@ -6,12 +6,14 @@ import type { PackageDependency } from '../../../types' import { pluginManifestToCardPluginProps } from '../../utils' import LoadedItem from './loaded-item' import LoadingError from '../../base/loading-error' +import type { VersionProps } from '@/app/components/plugins/types' type Props = { checked: boolean onCheckedChange: (plugin: Plugin) => void payload: PackageDependency isFromMarketPlace?: boolean + versionInfo: VersionProps } const PackageItem: FC = ({ @@ -19,6 +21,7 @@ const PackageItem: FC = ({ checked, onCheckedChange, isFromMarketPlace, + versionInfo, }) => { if (!payload.value?.manifest) return @@ -30,6 +33,7 @@ const PackageItem: FC = ({ checked={checked} onCheckedChange={onCheckedChange} isFromMarketPlace={isFromMarketPlace} + versionInfo={versionInfo} /> ) } diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx index e8bf71297d..7e406f1a95 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx @@ -1,10 +1,11 @@ 'use client' import type { FC } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react' -import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin } from '../../../types' +import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types' import MarketplaceItem from '../item/marketplace-item' import GithubItem from '../item/github-item' import { useFetchPluginsInMarketPlaceByIds, useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins' +import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' import produce from 'immer' import PackageItem from '../item/package-item' import LoadingError from '../../base/loading-error' @@ -13,7 +14,7 @@ type Props = { allPlugins: Dependency[] selectedPlugins: Plugin[] onSelect: (plugin: Plugin, selectedIndex: number) => void - onLoadedAllPlugin: () => void + onLoadedAllPlugin: (installedInfo: Record) => void isFromMarketPlace?: boolean } @@ -28,6 +29,7 @@ const InstallByDSLList: FC = ({ const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByIds(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value.plugin_unique_identifier!)) // has meta(org,name,version), to get id const { isLoading: isFetchingDataByMeta, data: infoByMeta, error: infoByMetaError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!)) + const [plugins, doSetPlugins] = useState<(Plugin | undefined)[]>((() => { const hasLocalPackage = allPlugins.some(d => d.type === 'package') if (!hasLocalPackage) @@ -45,6 +47,7 @@ const InstallByDSLList: FC = ({ }) return _plugins })()) + const pluginsRef = React.useRef<(Plugin | undefined)[]>(plugins) const setPlugins = useCallback((p: (Plugin | undefined)[]) => { @@ -132,11 +135,30 @@ const InstallByDSLList: FC = ({ }, [infoByMetaError, infoByIdError]) const isLoadedAllData = (plugins.filter(p => !!p).length + errorIndexes.length) === allPlugins.length + + const { installedInfo, isLoading: isLoadingCheckInstalled } = useCheckInstalled({ + pluginIds: plugins?.filter(p => !!p).map((d) => { + return `${d?.org}/${d?.name}` + }) || [], + enabled: isLoadedAllData, + }) + + const getVersionInfo = useCallback((pluginId: string) => { + const pluginDetail = installedInfo?.[pluginId] + const hasInstalled = !!pluginDetail + return { + hasInstalled, + installedVersion: pluginDetail?.installedVersion, + toInstallVersion: '', + } + }, [installedInfo]) + useEffect(() => { - if (isLoadedAllData) - onLoadedAllPlugin() + if (isLoadedAllData && installedInfo) + onLoadedAllPlugin(installedInfo!) + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoadedAllData]) + }, [isLoadedAllData, installedInfo]) const handleSelect = useCallback((index: number) => { return () => { @@ -151,6 +173,7 @@ const InstallByDSLList: FC = ({ ) } + const plugin = plugins[index] if (d.type === 'github') { return ( = ({ dependency={d as GitHubItemAndMarketPlaceDependency} onFetchedPayload={handleGitHubPluginFetched(index)} onFetchError={handleGitHubPluginFetchError(index)} + versionInfo={getVersionInfo(`${plugin?.org}/${plugin?.name}`)} />) } @@ -169,6 +193,8 @@ const InstallByDSLList: FC = ({ checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} onCheckedChange={handleSelect(index)} payload={plugins[index] as Plugin} + version={(d as GitHubItemAndMarketPlaceDependency).value.version!} + versionInfo={getVersionInfo(`${plugin?.org}/${plugin?.name}`)} /> ) } @@ -181,6 +207,7 @@ const InstallByDSLList: FC = ({ onCheckedChange={handleSelect(index)} payload={d as PackageDependency} isFromMarketPlace={isFromMarketPlace} + versionInfo={getVersionInfo(`${plugin?.org}/${plugin?.name}`)} /> ) }) diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx index f2bd2e86bf..7f33c5cef3 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx @@ -1,12 +1,12 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' -import type { Dependency, InstallStatusResponse, Plugin } from '../../../types' +import React, { useCallback, useState } from 'react' +import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types' import Button from '@/app/components/base/button' import { RiLoader2Line } from '@remixicon/react' import { useTranslation } from 'react-i18next' import InstallMulti from './install-multi' -import { useInstallFromMarketplaceAndGitHub } from '@/service/use-plugins' +import { useInstallOrUpdate } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' const i18nPrefix = 'plugin.installModal' @@ -43,12 +43,15 @@ const Install: FC = ({ } const [canInstall, setCanInstall] = React.useState(false) - const handleLoadedAllPlugin = useCallback(() => { + const [installedInfo, setInstalledInfo] = useState | undefined>(undefined) + + const handleLoadedAllPlugin = useCallback((installedInfo: Record | undefined) => { + setInstalledInfo(installedInfo) setCanInstall(true) }, []) // Install from marketplace and github - const { mutate: installFromMarketplaceAndGitHub, isPending: isInstalling } = useInstallFromMarketplaceAndGitHub({ + const { mutate: installOrUpdate, isPending: isInstalling } = useInstallOrUpdate({ onSuccess: (res: InstallStatusResponse[]) => { onInstalled(selectedPlugins, res.map((r, i) => { return ({ @@ -62,9 +65,10 @@ const Install: FC = ({ }, }) const handleInstall = () => { - installFromMarketplaceAndGitHub({ + installOrUpdate({ payload: allPlugins.filter((_d, index) => selectedIndexes.includes(index)), plugin: selectedPlugins, + installedInfo: installedInfo!, }) } return ( diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 1ab127ec55..338b160adf 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -359,3 +359,14 @@ export type Version = { export type VersionListResponse = { versions: Version[] } + +export type VersionInfo = { + installedVersion: string, + uniqueIdentifier: string +} + +export type VersionProps = { + hasInstalled: boolean + installedVersion?: string + toInstallVersion: string +} diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 50003a2f1a..667aaf13ff 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -12,6 +12,7 @@ import type { PluginTask, PluginsFromMarketplaceByInfoResponse, PluginsFromMarketplaceResponse, + VersionInfo, VersionListResponse, uploadGitHubResponse, } from '@/app/components/plugins/types' @@ -145,22 +146,30 @@ export const useUploadGitHub = (payload: { }) } -export const useInstallFromMarketplaceAndGitHub = ({ +export const useInstallOrUpdate = ({ onSuccess, }: { onSuccess?: (res: { success: boolean }[]) => void }) => { + const { mutateAsync: updatePackageFromMarketPlace } = useUpdatePackageFromMarketPlace() + return useMutation({ mutationFn: (data: { payload: Dependency[], plugin: Plugin[], + installedInfo: Record }) => { - const { payload, plugin } = data + const { payload, plugin, installedInfo } = data + return Promise.all(payload.map(async (item, i) => { try { + const orgAndName = `${plugin[i]?.org}/${plugin[i]?.name}` + const installedPayload = installedInfo[orgAndName] + const isInstalled = !!installedPayload + let uniqueIdentifier = '' + if (item.type === 'github') { const data = item as GitHubItemAndMarketPlaceDependency - let pluginId = '' // From local bundle don't have data.value.github_plugin_unique_identifier if (!data.value.github_plugin_unique_identifier) { const { unique_identifier } = await post('/workspaces/current/plugin/upload/github', { @@ -170,32 +179,61 @@ export const useInstallFromMarketplaceAndGitHub = ({ package: data.value.packages! || data.value.package!, }, }) - pluginId = unique_identifier + uniqueIdentifier = data.value.github_plugin_unique_identifier! || unique_identifier + // has the same version, but not installed + if (uniqueIdentifier === installedPayload?.uniqueIdentifier) { + return { + success: true, + } + } + } + if (!isInstalled) { + await post('/workspaces/current/plugin/install/github', { + body: { + repo: data.value.repo!, + version: data.value.release! || data.value.version!, + package: data.value.packages! || data.value.package!, + plugin_unique_identifier: uniqueIdentifier, + }, + }) } - await post('/workspaces/current/plugin/install/github', { - body: { - repo: data.value.repo!, - version: data.value.release! || data.value.version!, - package: data.value.packages! || data.value.package!, - plugin_unique_identifier: data.value.github_plugin_unique_identifier! || pluginId, - }, - }) } if (item.type === 'marketplace') { const data = item as GitHubItemAndMarketPlaceDependency - - await post('/workspaces/current/plugin/install/marketplace', { - body: { - plugin_unique_identifiers: [data.value.plugin_unique_identifier! || plugin[i]?.plugin_id], - }, - }) + uniqueIdentifier = data.value.plugin_unique_identifier! || plugin[i]?.plugin_id + if (uniqueIdentifier === installedPayload?.uniqueIdentifier) { + return { + success: true, + } + } + if (!isInstalled) { + await post('/workspaces/current/plugin/install/marketplace', { + body: { + plugin_unique_identifiers: [uniqueIdentifier], + }, + }) + } } if (item.type === 'package') { const data = item as PackageDependency - await post('/workspaces/current/plugin/install/pkg', { - body: { - plugin_unique_identifiers: [data.value.unique_identifier], - }, + uniqueIdentifier = data.value.unique_identifier + if (uniqueIdentifier === installedPayload?.uniqueIdentifier) { + return { + success: true, + } + } + if (!isInstalled) { + await post('/workspaces/current/plugin/install/pkg', { + body: { + plugin_unique_identifiers: [uniqueIdentifier], + }, + }) + } + } + if (isInstalled) { + await updatePackageFromMarketPlace({ + original_plugin_unique_identifier: installedPayload?.uniqueIdentifier, + new_plugin_unique_identifier: uniqueIdentifier, }) } return ({ success: true })