feat: install plugin from marketplace now supports permission check. install multiple plugins page supports select/deselect all

pull/20014/head
NFish 12 months ago
parent 7fec4a928f
commit a6a3a69a56

@ -3,42 +3,44 @@ import type { SystemFeatures } from '@/types/feature'
import { InstallationScope } from '@/types/feature' import { InstallationScope } from '@/types/feature'
import type { Plugin, PluginManifestInMarket } from '../../types' 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) { 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) { if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.ALL) {
return { return {
canInstall: true, canInstall: true,
}
} }
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) { }
return { if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) {
canInstall: false, return {
} canInstall: false,
} }
const verification = plugin.verification }
if (plugin.verification && !plugin.verification.authorized_category) const verification = plugin.verification
verification.authorized_category = 'langgenius' if (plugin.verification && !plugin.verification.authorized_category)
verification.authorized_category = 'langgenius'
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) { if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) {
return { return {
canInstall: verification.authorized_category === 'langgenius', 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_AND_PARTNER) {
return { 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) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return pluginInstallLimit(plugin, systemFeatures) return pluginInstallLimit(plugin, systemFeatures)
} }

@ -39,7 +39,7 @@ const Item: FC<Props> = ({
plugin_id: data.unique_identifier, plugin_id: data.unique_identifier,
} }
onFetchedPayload(payload) onFetchedPayload(payload)
setPayload(payload) setPayload({ ...payload, from: dependency.type })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]) }, [data])

@ -29,7 +29,7 @@ const PackageItem: FC<Props> = ({
const plugin = pluginManifestToCardPluginProps(payload.value.manifest) const plugin = pluginManifestToCardPluginProps(payload.value.manifest)
return ( return (
<LoadedItem <LoadedItem
payload={plugin} payload={{ ...plugin, from: payload.type }}
checked={checked} checked={checked}
onCheckedChange={onCheckedChange} onCheckedChange={onCheckedChange}
isFromMarketPlace={isFromMarketPlace} isFromMarketPlace={isFromMarketPlace}

@ -1,5 +1,6 @@
'use client' 'use client'
import type { FC } from 'react' import type { ForwardRefRenderFunction } from 'react'
import { useImperativeHandle } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types' import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
import MarketplaceItem from '../item/marketplace-item' import MarketplaceItem from '../item/marketplace-item'
@ -9,22 +10,34 @@ import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use
import produce from 'immer' import produce from 'immer'
import PackageItem from '../item/package-item' import PackageItem from '../item/package-item'
import LoadingError from '../../base/loading-error' import LoadingError from '../../base/loading-error'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
type Props = { type Props = {
allPlugins: Dependency[] allPlugins: Dependency[]
selectedPlugins: Plugin[] selectedPlugins: Plugin[]
onSelect: (plugin: Plugin, selectedIndex: number) => void onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void
onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void
onDeSelectAll: () => void
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
isFromMarketPlace?: boolean isFromMarketPlace?: boolean
} }
const InstallByDSLList: FC<Props> = ({ export type ExposeRefs = {
selectAllPlugins: () => void
deSelectAllPlugins: () => void
}
const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({
allPlugins, allPlugins,
selectedPlugins, selectedPlugins,
onSelect, onSelect,
onSelectAll,
onDeSelectAll,
onLoadedAllPlugin, onLoadedAllPlugin,
isFromMarketPlace, isFromMarketPlace,
}) => { }, ref) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
// DSL has id, to get plugin info to show more info // 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 { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const dependecy = (d as GitHubItemAndMarketPlaceDependency).value const dependecy = (d as GitHubItemAndMarketPlaceDependency).value
@ -97,7 +110,8 @@ const InstallByDSLList: FC<Props> = ({
const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => { const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const p = d as GitHubItemAndMarketPlaceDependency const p = d as GitHubItemAndMarketPlaceDependency
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0] 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 payloads = sortedList
const failedIndex: number[] = [] const failedIndex: number[] = []
@ -106,7 +120,7 @@ const InstallByDSLList: FC<Props> = ({
if (payloads[i]) { if (payloads[i]) {
draft[index] = { draft[index] = {
...payloads[i], ...payloads[i],
version: payloads[i].version || payloads[i].latest_version, version: payloads[i]!.version || payloads[i]!.latest_version,
} }
} }
else { failedIndex.push(index) } else { failedIndex.push(index) }
@ -181,9 +195,35 @@ const InstallByDSLList: FC<Props> = ({
const handleSelect = useCallback((index: number) => { const handleSelect = useCallback((index: number) => {
return () => { 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 ( return (
<> <>
{allPlugins.map((d, index) => { {allPlugins.map((d, index) => {
@ -211,7 +251,7 @@ const InstallByDSLList: FC<Props> = ({
key={index} key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)} onCheckedChange={handleSelect(index)}
payload={plugin} payload={{ ...plugin, from: d.type } as Plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''} version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)} versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/> />
@ -234,4 +274,4 @@ const InstallByDSLList: FC<Props> = ({
</> </>
) )
} }
export default React.memo(InstallByDSLList) export default React.forwardRef(InstallByDSLList)

@ -1,15 +1,18 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useRef } from 'react'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types' import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { RiLoader2Line } from '@remixicon/react' import { RiLoader2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ExposeRefs } from './install-multi'
import InstallMulti from './install-multi' import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins' import { useInstallOrUpdate } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
import { useMittContextSelector } from '@/context/mitt-context' import { useMittContextSelector } from '@/context/mitt-context'
import Checkbox from '@/app/components/base/checkbox'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
type Props = { type Props = {
@ -34,18 +37,8 @@ const Install: FC<Props> = ({
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([]) const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([]) const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length const selectedPluginsNum = selectedPlugins.length
const installMultiRef = useRef<ExposeRefs>(null)
const { refreshPluginList } = useRefreshPluginList() 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 [canInstall, setCanInstall] = React.useState(false)
const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined) const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined)
@ -81,6 +74,51 @@ const Install: FC<Props> = ({
installedInfo: installedInfo!, 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() const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace()
return ( return (
<> <>
@ -90,9 +128,12 @@ const Install: FC<Props> = ({
</div> </div>
<div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'> <div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'>
<InstallMulti <InstallMulti
ref={installMultiRef}
allPlugins={allPlugins} allPlugins={allPlugins}
selectedPlugins={selectedPlugins} selectedPlugins={selectedPlugins}
onSelect={handleSelect} onSelect={handleSelect}
onSelectAll={handleSelectAll}
onDeSelectAll={handleDeSelectAll}
onLoadedAllPlugin={handleLoadedAllPlugin} onLoadedAllPlugin={handleLoadedAllPlugin}
isFromMarketPlace={isFromMarketPlace} isFromMarketPlace={isFromMarketPlace}
/> />
@ -100,21 +141,29 @@ const Install: FC<Props> = ({
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}
{!isHideButton && ( {!isHideButton && (
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'> <div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'>
{!canInstall && ( <div className='px-2'>
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> {canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
{t('common.operation.cancel')} <Checkbox checked={isSelectAll} indeterminate={isIndeterminate} />
<p className='system-sm-medium cursor-pointer text-text-secondary'>{isSelectAll ? t('common.operation.deSelectAll') : t('common.operation.selectAll')}</p>
</div>}
</div>
<div className='flex items-center justify-end gap-2 self-stretch'>
{!canInstall && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
)}
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button> </Button>
)} </div>
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button>
</div> </div>
)} )}

@ -127,8 +127,7 @@ const Installed: FC<Props> = ({
return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
}, [langeniusVersionInfo.current_version, pluginDeclaration]) }, [langeniusVersionInfo.current_version, pluginDeclaration])
const { canInstall } = useInstallPluginLimit(payload) const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' })
return ( return (
<> <>
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>

@ -97,7 +97,8 @@ export type PluginManifestInMarket = {
badges: string[], badges: string[],
verification: { verification: {
authorized_category: 'langgenius' | 'partner' | 'community' authorized_category: 'langgenius' | 'partner' | 'community'
} },
from: Dependency['type']
} }
export type PluginDetail = { export type PluginDetail = {
@ -151,7 +152,8 @@ export type Plugin = {
badges: string[], badges: string[],
verification: { verification: {
authorized_category: 'langgenius' | 'partner' | 'community' authorized_category: 'langgenius' | 'partner' | 'community'
} },
from: Dependency['type']
} }
export enum PermissionType { export enum PermissionType {

@ -64,6 +64,8 @@ const translation = {
skip: 'Skip', skip: 'Skip',
format: 'Format', format: 'Format',
more: 'More', more: 'More',
selectAll: 'Select All',
deSelectAll: 'DeSelect All',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} is required', fieldRequired: '{{field}} is required',

@ -64,6 +64,8 @@ const translation = {
in: '中', in: '中',
format: 'フォーマット', format: 'フォーマット',
more: 'もっと', more: 'もっと',
selectAll: 'すべて選択',
deSelectAll: 'すべて選択解除',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}}は必要です', fieldRequired: '{{field}}は必要です',

@ -64,6 +64,8 @@ const translation = {
skip: '跳过', skip: '跳过',
format: '格式化', format: '格式化',
more: '更多', more: '更多',
selectAll: '全选',
deSelectAll: '取消全选',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} 为必填项', fieldRequired: '{{field}} 为必填项',

Loading…
Cancel
Save