-
-
- {isPartner &&
}
- {verified &&
}
- {titleLeft} {/* This can be version badge */}
+
+ {!hideCornerMark &&
}
+ {/* Header */}
+
+
+
+
+
+ {isPartner &&
}
+ {verified &&
}
+ {titleLeft} {/* This can be version badge */}
+
+
-
+
+ {footer &&
{footer}
}
-
- {footer &&
{footer}
}
+ {limitedInstall
+ &&
+
+
+ {t('plugin.installModal.installWarning')}
+
+
}
)
}
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
new file mode 100644
index 000000000..d668cb3d1
--- /dev/null
+++ b/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx
@@ -0,0 +1,46 @@
+import { useGlobalPublicStore } from '@/context/global-public-context'
+import type { SystemFeatures } from '@/types/feature'
+import { InstallationScope } from '@/types/feature'
+import type { Plugin, PluginManifestInMarket } from '../../types'
+
+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 (plugin.from === 'github' || plugin.from === 'package')
+ return { canInstall: false }
+ }
+
+ 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,
+ }
+ }
+ 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',
+ }
+ }
+ return {
+ canInstall: true,
+ }
+}
+
+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 96abaa2e1..48f0bff3c 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/loaded-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx
index 5eb4c94ab..cf336ae68 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
@@ -8,6 +8,7 @@ import useGetIcon from '../../base/use-get-icon'
import { MARKETPLACE_API_PREFIX } from '@/config'
import Version from '../../base/version'
import type { VersionProps } from '../../../types'
+import usePluginInstallLimit from '../../hooks/use-install-plugin-limit'
type Props = {
checked: boolean
@@ -29,9 +30,11 @@ const LoadedItem: FC = ({
...particleVersionInfo,
toInstallVersion: payload.version,
}
+ const { canInstall } = usePluginInstallLimit(payload)
return (
onCheckedChange(payload)}
@@ -43,6 +46,7 @@ const LoadedItem: FC = ({
icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon),
}}
titleLeft={payload.version ? : null}
+ limitedInstall={!canInstall}
/>
)
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 101c8faca..eac03011a 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 db24bdd97..2d8bdcd3d 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 = ({