diff --git a/web/app/components/base/badge.tsx b/web/app/components/base/badge.tsx
index c520fe02c9..c44057b9a4 100644
--- a/web/app/components/base/badge.tsx
+++ b/web/app/components/base/badge.tsx
@@ -3,7 +3,7 @@ import cn from '@/utils/classnames'
type BadgeProps = {
className?: string
- text: string
+ text: string | React.ReactNode
uppercase?: boolean
hasRedCornerMark?: boolean
}
diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx
index 97e61b66d8..024d5c2804 100644
--- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx
@@ -1,7 +1,8 @@
-import React, { useCallback, useMemo } from 'react'
+import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import {
+ RiArrowLeftRightLine,
RiBugLine,
RiCloseLine,
RiHardDrive3Line,
@@ -15,7 +16,9 @@ import Title from '../card/base/title'
import OrgInfo from '../card/base/org-info'
import { useGitHubReleases } from '../install-plugin/hooks'
import { compareVersion, getLatestVersion } from '@/utils/semver'
-import OperationDropdown from './operation-dropdown'
+import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'
+import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place'
+import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import PluginInfo from '@/app/components/plugins/plugin-page/plugin-info'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
@@ -28,7 +31,6 @@ import { Github } from '@/app/components/base/icons/src/public/common'
import { uninstallPlugin } from '@/service/plugins'
import { useGetLanguage } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
-import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames'
@@ -58,11 +60,17 @@ const DetailHeader = ({
latest_unique_identifier,
latest_version,
meta,
+ plugin_id,
} = detail
const { author, name, label, description, icon, verified } = detail.declaration
const isFromGitHub = source === PluginSource.github
const isFromMarketplace = source === PluginSource.marketplace
+ const [isShow, setIsShow] = useState(false)
+ const [targetVersion, setTargetVersion] = useState({
+ version: latest_version,
+ unique_identifier: latest_unique_identifier,
+ })
const hasNewVersion = useMemo(() => {
if (isFromGitHub)
return latest_version !== version
@@ -167,10 +175,33 @@ const DetailHeader = ({
{verified &&
}
-
{
+ setTargetVersion(state)
+ handleUpdate()
+ }}
+ trigger={
+
+ {version}
+ {isFromMarketplace && }
+ >
+ }
+ hasRedCornerMark={hasNewVersion}
+ />
+ }
/>
{hasNewVersion && (
@@ -254,8 +285,8 @@ const DetailHeader = ({
payload: detail.declaration,
},
targetPackageInfo: {
- id: latest_unique_identifier,
- version: latest_version,
+ id: targetVersion.unique_identifier,
+ version: targetVersion.version,
},
}}
onCancel={hideUpdateModal}
diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx
index 0dfc65404f..812cdb8e20 100644
--- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx
@@ -65,6 +65,7 @@ const EndpointList = ({ showTopBorder }: Props) => {
{t('plugin.detailPanel.endpoints')}
void
+ pluginID: string
+ currentVersion: string
+ trigger: React.ReactNode
+ placement?: Placement
+ offset?: OffsetOptions
+ onSelect: ({
+ version,
+ unique_identifier,
+ }: {
+ version: string
+ unique_identifier: string
+ }) => void
+}
+
+const PluginVersionPicker: FC = ({
+ disabled = false,
+ isShow,
+ onShowChange,
+ pluginID,
+ currentVersion,
+ trigger,
+ placement = 'bottom-start',
+ offset = {
+ mainAxis: 4,
+ crossAxis: -16,
+ },
+ onSelect,
+}) => {
+ const { t } = useTranslation()
+ const format = t('appLog.dateTimeFormat').split(' ')[0]
+ const { formatDate } = useTimestamp()
+
+ const handleTriggerClick = () => {
+ if (disabled) return
+ onShowChange(true)
+ }
+
+ const { data: res } = useVersionListOfPlugin(pluginID)
+
+ const handleSelect = useCallback(({ version, unique_identifier }: {
+ version: string
+ unique_identifier: string
+ }) => {
+ if (currentVersion === version)
+ return
+ onSelect({ version, unique_identifier })
+ onShowChange(false)
+ }, [currentVersion, onSelect])
+
+ return (
+
+
+ {trigger}
+
+
+
+
+
+ {t('plugin.detailPanel.switchVersion')}
+
+
+ {res?.data.versions.map(version => (
+
handleSelect({
+ version: version.version,
+ unique_identifier: version.unique_identifier,
+ })}
+ >
+
+
{version.version}
+ {currentVersion === version.version &&
}
+
+
{formatDate(version.created_at, format)}
+
+ ))}
+
+
+
+
+ )
+}
+
+export default React.memo(PluginVersionPicker)
diff --git a/web/hooks/use-timestamp.ts b/web/hooks/use-timestamp.ts
index 05cc48eaad..5242eb565a 100644
--- a/web/hooks/use-timestamp.ts
+++ b/web/hooks/use-timestamp.ts
@@ -15,7 +15,11 @@ const useTimestamp = () => {
return dayjs.unix(value).tz(timezone).format(format)
}, [timezone])
- return { formatTime }
+ const formatDate = useCallback((value: string, format: string) => {
+ return dayjs(value).tz(timezone).format(format)
+ }, [timezone])
+
+ return { formatTime, formatDate }
}
export default useTimestamp
diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts
index e2f3ae7cf5..0e8e6dfccd 100644
--- a/web/i18n/en-US/plugin.ts
+++ b/web/i18n/en-US/plugin.ts
@@ -33,6 +33,7 @@ const translation = {
local: 'Local Package File',
},
detailPanel: {
+ switchVersion: 'Switch Version',
categoryTip: {
marketplace: 'Installed from Marketplace',
github: 'Installed from Github',
diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts
index c537656e54..c1ad4e0d67 100644
--- a/web/i18n/zh-Hans/plugin.ts
+++ b/web/i18n/zh-Hans/plugin.ts
@@ -33,6 +33,7 @@ const translation = {
local: '本地插件',
},
detailPanel: {
+ switchVersion: '切换版本',
categoryTip: {
marketplace: '从 Marketplace 安装',
github: '从 Github 安装',
diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts
index 8ab970db13..8c45f35301 100644
--- a/web/service/use-plugins.ts
+++ b/web/service/use-plugins.ts
@@ -7,13 +7,14 @@ import type {
Permissions,
PluginTask,
PluginsFromMarketplaceResponse,
+ VersionListResponse,
uploadGitHubResponse,
} from '@/app/components/plugins/types'
import { TaskStatus } from '@/app/components/plugins/types'
import type {
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
-import { get, post, postMarketplace } from './base'
+import { get, getMarketplace, post, postMarketplace } from './base'
import {
useMutation,
useQuery,
@@ -52,6 +53,19 @@ export const useInstallPackageFromMarketPlace = () => {
})
}
+export const useVersionListOfPlugin = (pluginID: string) => {
+ return useQuery<{ data: VersionListResponse }>({
+ queryKey: [NAME_SPACE, 'versions', pluginID],
+ queryFn: () => getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, { params: { page: 1, page_size: 100 } }),
+ })
+}
+export const useInvalidateVersionListOfPlugin = () => {
+ const queryClient = useQueryClient()
+ return (pluginID: string) => {
+ queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'versions', pluginID] })
+ }
+}
+
export const useInstallPackageFromLocal = () => {
return useMutation({
mutationFn: (uniqueIdentifier: string) => {