From a6a3a69a5697c6716830237d55208d85ab7243a4 Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 10 Jun 2025 18:55:40 +0800 Subject: [PATCH] feat: install plugin from marketplace now supports permission check. install multiple plugins page supports select/deselect all --- .../hooks/use-install-plugin-limit.tsx | 52 +++++----- .../install-bundle/item/github-item.tsx | 2 +- .../install-bundle/item/package-item.tsx | 2 +- .../install-bundle/steps/install-multi.tsx | 60 +++++++++-- .../install-bundle/steps/install.tsx | 99 ++++++++++++++----- .../steps/install.tsx | 3 +- web/app/components/plugins/types.ts | 6 +- web/i18n/en-US/common.ts | 2 + web/i18n/ja-JP/common.ts | 2 + web/i18n/zh-Hans/common.ts | 2 + 10 files changed, 164 insertions(+), 66 deletions(-) diff --git a/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx b/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx index 1f91efc3af..0c4ca92c81 100644 --- a/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx +++ b/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx @@ -3,42 +3,44 @@ import type { SystemFeatures } from '@/types/feature' import { InstallationScope } from '@/types/feature' import type { Plugin, PluginManifestInMarket } from '../../types' -export function pluginInstallLimit(plugin: Plugin | PluginManifestInMarket, systemFeatures: SystemFeatures) { +type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' } + +export function pluginInstallLimit(plugin: PluginProps, systemFeatures: SystemFeatures) { if (systemFeatures.plugin_installation_permission.restrict_to_marketplace_only) { - return { canInstall: false } + if (plugin.from === 'github' || plugin.from === 'package') + return { canInstall: false } } - else { - if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.ALL) { - return { - canInstall: true, - } + + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.ALL) { + return { + canInstall: true, } - if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) { - return { - canInstall: false, - } + } + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) { + return { + canInstall: false, } - const verification = plugin.verification - if (plugin.verification && !plugin.verification.authorized_category) - verification.authorized_category = 'langgenius' + } + const verification = plugin.verification + if (plugin.verification && !plugin.verification.authorized_category) + verification.authorized_category = 'langgenius' - if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) { - return { - canInstall: verification.authorized_category === 'langgenius', - } - } - if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_AND_PARTNER) { - return { - canInstall: verification.authorized_category === 'langgenius' || verification.authorized_category === 'partner', - } + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) { + return { + canInstall: verification.authorized_category === 'langgenius', } + } + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_AND_PARTNER) { return { - canInstall: true, + canInstall: verification.authorized_category === 'langgenius' || verification.authorized_category === 'partner', } } + return { + canInstall: true, + } } -export default function usePluginInstallLimit(plugin: Plugin | PluginManifestInMarket) { +export default function usePluginInstallLimit(plugin: PluginProps) { const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return pluginInstallLimit(plugin, systemFeatures) } 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 96abaa2e1c..48f0bff3c7 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 @@ -39,7 +39,7 @@ const Item: FC = ({ plugin_id: data.unique_identifier, } onFetchedPayload(payload) - setPayload(payload) + setPayload({ ...payload, from: dependency.type }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]) 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 101c8facaf..eac03011ad 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 @@ -29,7 +29,7 @@ const PackageItem: FC = ({ const plugin = pluginManifestToCardPluginProps(payload.value.manifest) return ( void + onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void + onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void + onDeSelectAll: () => void onLoadedAllPlugin: (installedInfo: Record) => void isFromMarketPlace?: boolean } -const InstallByDSLList: FC = ({ +export type ExposeRefs = { + selectAllPlugins: () => void + deSelectAllPlugins: () => void +} + +const InstallByDSLList: ForwardRefRenderFunction = ({ allPlugins, selectedPlugins, onSelect, + onSelectAll, + onDeSelectAll, onLoadedAllPlugin, isFromMarketPlace, -}) => { +}, ref) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) // DSL has id, to get plugin info to show more info const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => { const dependecy = (d as GitHubItemAndMarketPlaceDependency).value @@ -97,7 +110,8 @@ const InstallByDSLList: FC = ({ const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => { const p = d as GitHubItemAndMarketPlaceDependency const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0] - return infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin + const retPluginInfo = infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin + return { ...retPluginInfo, from: d.type } as Plugin }) const payloads = sortedList const failedIndex: number[] = [] @@ -106,7 +120,7 @@ const InstallByDSLList: FC = ({ if (payloads[i]) { draft[index] = { ...payloads[i], - version: payloads[i].version || payloads[i].latest_version, + version: payloads[i]!.version || payloads[i]!.latest_version, } } else { failedIndex.push(index) } @@ -181,9 +195,35 @@ const InstallByDSLList: FC = ({ const handleSelect = useCallback((index: number) => { return () => { - onSelect(plugins[index]!, index) + const canSelectPlugins = plugins.filter((p) => { + const { canInstall } = pluginInstallLimit(p!, systemFeatures) + return canInstall + }) + onSelect(plugins[index]!, index, canSelectPlugins.length) } - }, [onSelect, plugins]) + }, [onSelect, plugins, systemFeatures]) + + useImperativeHandle(ref, () => ({ + selectAllPlugins: () => { + const selectedIndexes: number[] = [] + const selectedPlugins: Plugin[] = [] + allPlugins.forEach((d, index) => { + const p = plugins[index] + if (!p) + return + const { canInstall } = pluginInstallLimit(p, systemFeatures) + if (canInstall) { + selectedIndexes.push(index) + selectedPlugins.push(p) + } + }) + onSelectAll(selectedPlugins, selectedIndexes) + }, + deSelectAllPlugins: () => { + onDeSelectAll() + }, + })) + return ( <> {allPlugins.map((d, index) => { @@ -211,7 +251,7 @@ const InstallByDSLList: FC = ({ key={index} checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} onCheckedChange={handleSelect(index)} - payload={plugin} + payload={{ ...plugin, from: d.type } as Plugin} version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''} versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)} /> @@ -234,4 +274,4 @@ const InstallByDSLList: FC = ({ ) } -export default React.memo(InstallByDSLList) +export default React.forwardRef(InstallByDSLList) 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 db24bdd97a..2d8bdcd3d9 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,15 +1,18 @@ 'use client' import type { FC } from 'react' +import { useRef } from 'react' 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 type { ExposeRefs } from './install-multi' import InstallMulti from './install-multi' import { useInstallOrUpdate } from '@/service/use-plugins' import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' import { useMittContextSelector } from '@/context/mitt-context' +import Checkbox from '@/app/components/base/checkbox' const i18nPrefix = 'plugin.installModal' type Props = { @@ -34,18 +37,8 @@ const Install: FC = ({ const [selectedPlugins, setSelectedPlugins] = React.useState([]) const [selectedIndexes, setSelectedIndexes] = React.useState([]) const selectedPluginsNum = selectedPlugins.length + const installMultiRef = useRef(null) const { refreshPluginList } = useRefreshPluginList() - const handleSelect = (plugin: Plugin, selectedIndex: number) => { - const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id) - let nextSelectedPlugins - if (isSelected) - nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id) - else - nextSelectedPlugins = [...selectedPlugins, plugin] - setSelectedPlugins(nextSelectedPlugins) - const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex] - setSelectedIndexes(nextSelectedIndexes) - } const [canInstall, setCanInstall] = React.useState(false) const [installedInfo, setInstalledInfo] = useState | undefined>(undefined) @@ -81,6 +74,51 @@ const Install: FC = ({ installedInfo: installedInfo!, }) } + const [isSelectAll, setIsSelectAll] = useState(false) + const [isIndeterminate, setIsIndeterminate] = useState(false) + const handleClickSelectAll = useCallback(() => { + if (isSelectAll) + installMultiRef.current?.deSelectAllPlugins() + else + installMultiRef.current?.selectAllPlugins() + }, [isSelectAll]) + const handleSelectAll = useCallback((plugins: Plugin[], selectedIndexes: number[]) => { + setSelectedPlugins(plugins) + setSelectedIndexes(selectedIndexes) + setIsSelectAll(true) + setIsIndeterminate(false) + }, []) + const handleDeSelectAll = useCallback(() => { + setSelectedPlugins([]) + setSelectedIndexes([]) + setIsSelectAll(false) + setIsIndeterminate(false) + }, []) + + const handleSelect = useCallback((plugin: Plugin, selectedIndex: number, allPluginsLength: number) => { + const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id) + let nextSelectedPlugins + if (isSelected) + nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id) + else + nextSelectedPlugins = [...selectedPlugins, plugin] + setSelectedPlugins(nextSelectedPlugins) + const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex] + setSelectedIndexes(nextSelectedIndexes) + if (nextSelectedPlugins.length === 0) { + setIsSelectAll(false) + setIsIndeterminate(false) + } + else if (nextSelectedPlugins.length === allPluginsLength) { + setIsSelectAll(true) + setIsIndeterminate(false) + } + else { + setIsIndeterminate(true) + setIsSelectAll(false) + } + }, [selectedPlugins, selectedIndexes]) + const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace() return ( <> @@ -90,9 +128,12 @@ const Install: FC = ({
@@ -100,21 +141,29 @@ const Install: FC = ({
{/* Action Buttons */} {!isHideButton && ( -
- {!canInstall && ( - + )} + - )} - +
)} diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index 58ae488ab8..dbc7c97d88 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -127,8 +127,7 @@ const Installed: FC = ({ return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') }, [langeniusVersionInfo.current_version, pluginDeclaration]) - const { canInstall } = useInstallPluginLimit(payload) - + const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' }) return ( <>
diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index e9667bdfb7..ac62d77437 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -97,7 +97,8 @@ export type PluginManifestInMarket = { badges: string[], verification: { authorized_category: 'langgenius' | 'partner' | 'community' - } + }, + from: Dependency['type'] } export type PluginDetail = { @@ -151,7 +152,8 @@ export type Plugin = { badges: string[], verification: { authorized_category: 'langgenius' | 'partner' | 'community' - } + }, + from: Dependency['type'] } export enum PermissionType { diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 05000f9343..bafe2b2ac9 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -64,6 +64,8 @@ const translation = { skip: 'Skip', format: 'Format', more: 'More', + selectAll: 'Select All', + deSelectAll: 'DeSelect All', }, errorMsg: { fieldRequired: '{{field}} is required', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 85f5863761..ca33863783 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -64,6 +64,8 @@ const translation = { in: '中', format: 'フォーマット', more: 'もっと', + selectAll: 'すべて選択', + deSelectAll: 'すべて選択解除', }, errorMsg: { fieldRequired: '{{field}}は必要です', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 00c8a33837..446a3d9533 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -64,6 +64,8 @@ const translation = { skip: '跳过', format: '格式化', more: '更多', + selectAll: '全选', + deSelectAll: '取消全选', }, errorMsg: { fieldRequired: '{{field}} 为必填项',