diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts
index 34cc491481437..27f187d1f7571 100644
--- a/web/app/components/tools/types.ts
+++ b/web/app/components/tools/types.ts
@@ -48,6 +48,7 @@ export type Collection = {
is_team_authorization: boolean
allow_delete: boolean
labels: string[]
+ plugin_id?: string
}
export type ToolParameter = {
diff --git a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx
index 6f0c08eeca967..f4a9668c3ebdf 100644
--- a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx
+++ b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx
@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
-import React, { useCallback, useRef } from 'react'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
@@ -12,12 +12,15 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
import { MARKETPLACE_URL_PREFIX } from '@/config'
+import { useDownloadPlugin } from '@/service/use-plugins'
+import { downloadFile } from '@/utils/format'
type Props = {
open: boolean
onOpenChange: (v: boolean) => void
author: string
name: string
+ version: string
}
const OperationDropdown: FC
= ({
@@ -25,6 +28,7 @@ const OperationDropdown: FC = ({
onOpenChange,
author,
name,
+ version,
}) => {
const { t } = useTranslation()
const openRef = useRef(open)
@@ -37,6 +41,25 @@ const OperationDropdown: FC = ({
setOpen(!openRef.current)
}, [setOpen])
+ const [needDownload, setNeedDownload] = useState(false)
+ const { data: blob, isLoading } = useDownloadPlugin({
+ organization: author,
+ pluginName: name,
+ version,
+ }, needDownload)
+ const handleDownload = useCallback(() => {
+ if (isLoading) return
+ setNeedDownload(true)
+ }, [isLoading])
+
+ useEffect(() => {
+ if (blob) {
+ const fileName = `${author}-${name}_${version}.zip`
+ downloadFile({ data: blob, fileName })
+ setNeedDownload(false)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [blob])
return (
= ({
diff --git a/web/app/components/workflow/block-selector/market-place-plugin/item.tsx b/web/app/components/workflow/block-selector/market-place-plugin/item.tsx
index 7f2ae3408362f..ebe4da73f8b70 100644
--- a/web/app/components/workflow/block-selector/market-place-plugin/item.tsx
+++ b/web/app/components/workflow/block-selector/market-place-plugin/item.tsx
@@ -59,6 +59,7 @@ const Item: FC = ({
onOpenChange={setOpen}
author={payload.org}
name={payload.name}
+ version={payload.latest_version}
/>
{isShowInstallModal && (
diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts
index bcbb1648f412e..88c21c0dd3f74 100644
--- a/web/i18n/en-US/plugin.ts
+++ b/web/i18n/en-US/plugin.ts
@@ -94,6 +94,7 @@ const translation = {
},
installModal: {
installPlugin: 'Install Plugin',
+ installComplete: 'Installation complete',
installedSuccessfully: 'Installation successful',
installedSuccessfullyDesc: 'The plugin has been installed successfully.',
uploadFailed: 'Upload failed',
diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts
index b293f99f8f0f6..94e1324bedcbc 100644
--- a/web/i18n/zh-Hans/plugin.ts
+++ b/web/i18n/zh-Hans/plugin.ts
@@ -94,6 +94,7 @@ const translation = {
},
installModal: {
installPlugin: '安装插件',
+ installComplete: '安装完成',
installedSuccessfully: '安装成功',
installedSuccessfullyDesc: '插件已成功安装。',
uploadFailed: '上传失败',
diff --git a/web/service/fetch.ts b/web/service/fetch.ts
index 666a3e2336db8..39f91f903f33d 100644
--- a/web/service/fetch.ts
+++ b/web/service/fetch.ts
@@ -12,6 +12,7 @@ export const ContentType = {
audio: 'audio/mpeg',
form: 'application/x-www-form-urlencoded; charset=UTF-8',
download: 'application/octet-stream', // for download
+ downloadZip: 'application/zip', // for download
upload: 'multipart/form-data', // for upload
}
@@ -193,7 +194,7 @@ async function base
(url: string, options: FetchOptionType = {}, otherOptions:
const contentType = res.headers.get('content-type')
if (
contentType
- && [ContentType.download, ContentType.audio].includes(contentType)
+ && [ContentType.download, ContentType.audio, ContentType.downloadZip].includes(contentType)
)
return await res.blob() as T
diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts
index 0c05290e0ffff..78c4ff770f27a 100644
--- a/web/service/use-plugins.ts
+++ b/web/service/use-plugins.ts
@@ -344,3 +344,12 @@ export const useMutationCheckDependenciesBeforeImportDSL = () => {
return mutation
}
+
+export const useDownloadPlugin = (info: { organization: string; pluginName: string; version: string }, needDownload: boolean) => {
+ return useQuery({
+ queryKey: [NAME_SPACE, 'downloadPlugin', info],
+ queryFn: () => getMarketplace(`/plugins/${info.organization}/${info.pluginName}/${info.version}/download`),
+ enabled: needDownload,
+ retry: 0,
+ })
+}
diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts
index 337f0bbfb8bad..bf9678332f456 100644
--- a/web/service/use-tools.ts
+++ b/web/service/use-tools.ts
@@ -21,6 +21,10 @@ export const useAllToolProviders = () => {
})
}
+export const useInvalidateAllToolProviders = () => {
+ return useInvalid(useAllToolProvidersKey)
+}
+
const useAllBuiltInToolsKey = [NAME_SPACE, 'builtIn']
export const useAllBuiltInTools = () => {
return useQuery({
diff --git a/web/utils/format.ts b/web/utils/format.ts
index 1eeb6af807e93..45f0e51878a52 100644
--- a/web/utils/format.ts
+++ b/web/utils/format.ts
@@ -34,3 +34,14 @@ export const formatTime = (num: number) => {
}
return `${num.toFixed(2)} ${units[index]}`
}
+
+export const downloadFile = ({ data, fileName }: { data: Blob; fileName: string }) => {
+ const url = window.URL.createObjectURL(data)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = fileName
+ document.body.appendChild(a)
+ a.click()
+ a.remove()
+ window.URL.revokeObjectURL(url)
+}