Feat: add check before install plugin (#20014)
This commit is contained in:
@@ -17,9 +17,9 @@ import Loading from '@/app/components/base/loading'
|
|||||||
import ProviderCard from '@/app/components/plugins/provider-card'
|
import ProviderCard from '@/app/components/plugins/provider-card'
|
||||||
import List from '@/app/components/plugins/marketplace/list'
|
import List from '@/app/components/plugins/marketplace/list'
|
||||||
import type { Plugin } from '@/app/components/plugins/types'
|
import type { Plugin } from '@/app/components/plugins/types'
|
||||||
import { MARKETPLACE_URL_PREFIX } from '@/config'
|
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { getLocaleOnClient } from '@/i18n'
|
import { getLocaleOnClient } from '@/i18n'
|
||||||
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
type InstallFromMarketplaceProps = {
|
type InstallFromMarketplaceProps = {
|
||||||
providers: ModelProvider[]
|
providers: ModelProvider[]
|
||||||
@@ -55,7 +55,7 @@ const InstallFromMarketplace = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='mb-2 flex items-center pt-2'>
|
<div className='mb-2 flex items-center pt-2'>
|
||||||
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
|
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
|
||||||
<Link target="_blank" href={`${MARKETPLACE_URL_PREFIX}${theme ? `?theme=${theme}` : ''}`} className='system-sm-medium inline-flex items-center text-text-accent'>
|
<Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'>
|
||||||
{t('plugin.marketplace.difyMarketplace')}
|
{t('plugin.marketplace.difyMarketplace')}
|
||||||
<RiArrowRightUpLine className='h-4 w-4' />
|
<RiArrowRightUpLine className='h-4 w-4' />
|
||||||
</Link>
|
</Link>
|
||||||
|
@@ -15,6 +15,7 @@ import { renderI18nObject } from '@/i18n'
|
|||||||
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
|
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
|
||||||
import Partner from '../base/badges/partner'
|
import Partner from '../base/badges/partner'
|
||||||
import Verified from '../base/badges/verified'
|
import Verified from '../base/badges/verified'
|
||||||
|
import { RiAlertFill } from '@remixicon/react'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -28,6 +29,7 @@ export type Props = {
|
|||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
loadingFileName?: string
|
loadingFileName?: string
|
||||||
locale?: string
|
locale?: string
|
||||||
|
limitedInstall?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Card = ({
|
const Card = ({
|
||||||
@@ -42,6 +44,7 @@ const Card = ({
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
loadingFileName,
|
loadingFileName,
|
||||||
locale: localeFromProps,
|
locale: localeFromProps,
|
||||||
|
limitedInstall = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const defaultLocale = useGetLanguage()
|
const defaultLocale = useGetLanguage()
|
||||||
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
|
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
|
||||||
@@ -54,7 +57,7 @@ const Card = ({
|
|||||||
obj ? renderI18nObject(obj, locale) : ''
|
obj ? renderI18nObject(obj, locale) : ''
|
||||||
const isPartner = badges.includes('partner')
|
const isPartner = badges.includes('partner')
|
||||||
|
|
||||||
const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs', className)
|
const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', className)
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Placeholder
|
<Placeholder
|
||||||
@@ -66,30 +69,39 @@ const Card = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={wrapClassName}>
|
<div className={wrapClassName}>
|
||||||
{!hideCornerMark && <CornerMark text={cornerMark} />}
|
<div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}>
|
||||||
{/* Header */}
|
{!hideCornerMark && <CornerMark text={cornerMark} />}
|
||||||
<div className="flex">
|
{/* Header */}
|
||||||
<Icon src={icon} installed={installed} installFailed={installFailed} />
|
<div className="flex">
|
||||||
<div className="ml-3 w-0 grow">
|
<Icon src={icon} installed={installed} installFailed={installFailed} />
|
||||||
<div className="flex h-5 items-center">
|
<div className="ml-3 w-0 grow">
|
||||||
<Title title={getLocalizedText(label)} />
|
<div className="flex h-5 items-center">
|
||||||
{isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />}
|
<Title title={getLocalizedText(label)} />
|
||||||
{verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />}
|
{isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />}
|
||||||
{titleLeft} {/* This can be version badge */}
|
{verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />}
|
||||||
|
{titleLeft} {/* This can be version badge */}
|
||||||
|
</div>
|
||||||
|
<OrgInfo
|
||||||
|
className="mt-0.5"
|
||||||
|
orgName={org}
|
||||||
|
packageName={name}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<OrgInfo
|
|
||||||
className="mt-0.5"
|
|
||||||
orgName={org}
|
|
||||||
packageName={name}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Description
|
||||||
|
className="mt-3"
|
||||||
|
text={getLocalizedText(brief)}
|
||||||
|
descriptionLineRows={descriptionLineRows}
|
||||||
|
/>
|
||||||
|
{footer && <div>{footer}</div>}
|
||||||
</div>
|
</div>
|
||||||
<Description
|
{limitedInstall
|
||||||
className="mt-3"
|
&& <div className='relative flex h-8 items-center gap-x-2 px-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:top-0 after:bg-toast-warning-bg after:opacity-40'>
|
||||||
text={getLocalizedText(brief)}
|
<RiAlertFill className='h-3 w-3 shrink-0 text-text-warning-secondary' />
|
||||||
descriptionLineRows={descriptionLineRows}
|
<p className='system-xs-regular z-10 grow text-text-secondary'>
|
||||||
/>
|
{t('plugin.installModal.installWarning')}
|
||||||
{footer && <div>{footer}</div>}
|
</p>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
@@ -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])
|
||||||
|
@@ -8,6 +8,7 @@ import useGetIcon from '../../base/use-get-icon'
|
|||||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||||
import Version from '../../base/version'
|
import Version from '../../base/version'
|
||||||
import type { VersionProps } from '../../../types'
|
import type { VersionProps } from '../../../types'
|
||||||
|
import usePluginInstallLimit from '../../hooks/use-install-plugin-limit'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
checked: boolean
|
checked: boolean
|
||||||
@@ -29,9 +30,11 @@ const LoadedItem: FC<Props> = ({
|
|||||||
...particleVersionInfo,
|
...particleVersionInfo,
|
||||||
toInstallVersion: payload.version,
|
toInstallVersion: payload.version,
|
||||||
}
|
}
|
||||||
|
const { canInstall } = usePluginInstallLimit(payload)
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center space-x-2'>
|
<div className='flex items-center space-x-2'>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
disabled={!canInstall}
|
||||||
className='shrink-0'
|
className='shrink-0'
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onCheck={() => onCheckedChange(payload)}
|
onCheck={() => onCheckedChange(payload)}
|
||||||
@@ -43,6 +46,7 @@ const LoadedItem: FC<Props> = ({
|
|||||||
icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon),
|
icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon),
|
||||||
}}
|
}}
|
||||||
titleLeft={payload.version ? <Version {...versionInfo} /> : null}
|
titleLeft={payload.version ? <Version {...versionInfo} /> : null}
|
||||||
|
limitedInstall={!canInstall}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -124,7 +124,7 @@ const Installed: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
{!isDifyVersionCompatible && (
|
{!isDifyVersionCompatible && (
|
||||||
<p className='system-md-regular flex items-center gap-1 text-text-secondary text-text-warning'>
|
<p className='system-md-regular flex items-center gap-1 text-text-warning'>
|
||||||
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })}
|
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@@ -15,6 +15,7 @@ import Version from '../../base/version'
|
|||||||
import { usePluginTaskList } from '@/service/use-plugins'
|
import { usePluginTaskList } from '@/service/use-plugins'
|
||||||
import { gte } from 'semver'
|
import { gte } from 'semver'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import useInstallPluginLimit from '../../hooks/use-install-plugin-limit'
|
||||||
|
|
||||||
const i18nPrefix = 'plugin.installModal'
|
const i18nPrefix = 'plugin.installModal'
|
||||||
|
|
||||||
@@ -124,15 +125,16 @@ const Installed: FC<Props> = ({
|
|||||||
const isDifyVersionCompatible = useMemo(() => {
|
const isDifyVersionCompatible = useMemo(() => {
|
||||||
if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true
|
if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true
|
||||||
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?.manifest.meta.minimum_dify_version])
|
}, [langeniusVersionInfo.current_version, pluginDeclaration])
|
||||||
|
|
||||||
|
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'>
|
||||||
<div className='system-md-regular text-text-secondary'>
|
<div className='system-md-regular text-text-secondary'>
|
||||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||||
{!isDifyVersionCompatible && (
|
{!isDifyVersionCompatible && (
|
||||||
<p className='system-md-regular text-text-secondary text-text-warning'>
|
<p className='system-md-regular text-text-warning'>
|
||||||
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
|
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -146,6 +148,7 @@ const Installed: FC<Props> = ({
|
|||||||
installedVersion={installedVersion}
|
installedVersion={installedVersion}
|
||||||
toInstallVersion={toInstallVersion}
|
toInstallVersion={toInstallVersion}
|
||||||
/>}
|
/>}
|
||||||
|
limitedInstall={!canInstall}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +162,7 @@ const Installed: FC<Props> = ({
|
|||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='primary'
|
||||||
className='flex min-w-[72px] space-x-0.5'
|
className='flex min-w-[72px] space-x-0.5'
|
||||||
disabled={isInstalling || isLoading}
|
disabled={isInstalling || isLoading || !canInstall}
|
||||||
onClick={handleInstall}
|
onClick={handleInstall}
|
||||||
>
|
>
|
||||||
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
|
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types'
|
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types'
|
||||||
import type { GitHubUrlInfo } from '@/app/components/plugins/types'
|
import type { GitHubUrlInfo } from '@/app/components/plugins/types'
|
||||||
|
import { isEmpty } from 'lodash-es'
|
||||||
|
|
||||||
export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => {
|
export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => {
|
||||||
return {
|
return {
|
||||||
@@ -47,6 +48,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife
|
|||||||
},
|
},
|
||||||
tags: [],
|
tags: [],
|
||||||
badges: pluginManifest.badges,
|
badges: pluginManifest.badges,
|
||||||
|
verification: isEmpty(pluginManifest.verification) ? { authorized_category: 'langgenius' } : pluginManifest.verification,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -56,7 +56,7 @@ const CardWrapper = ({
|
|||||||
>
|
>
|
||||||
{t('plugin.detailPanel.operation.install')}
|
{t('plugin.detailPanel.operation.install')}
|
||||||
</Button>
|
</Button>
|
||||||
<a href={`${getPluginLinkInMarketplace(plugin)}?language=${localeFromLocale}${theme ? `&theme=${theme}` : ''}`} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'>
|
<a href={getPluginLinkInMarketplace(plugin, { language: localeFromLocale, theme })} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'>
|
||||||
<Button
|
<Button
|
||||||
className='w-full gap-0.5'
|
className='w-full gap-0.5'
|
||||||
>
|
>
|
||||||
|
@@ -8,8 +8,8 @@ import type {
|
|||||||
} from '@/app/components/plugins/marketplace/types'
|
} from '@/app/components/plugins/marketplace/types'
|
||||||
import {
|
import {
|
||||||
MARKETPLACE_API_PREFIX,
|
MARKETPLACE_API_PREFIX,
|
||||||
MARKETPLACE_URL_PREFIX,
|
|
||||||
} from '@/config'
|
} from '@/config'
|
||||||
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
export const getPluginIconInMarketplace = (plugin: Plugin) => {
|
export const getPluginIconInMarketplace = (plugin: Plugin) => {
|
||||||
if (plugin.type === 'bundle')
|
if (plugin.type === 'bundle')
|
||||||
@@ -32,10 +32,10 @@ export const getFormattedPlugin = (bundle: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPluginLinkInMarketplace = (plugin: Plugin) => {
|
export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record<string, string | undefined>) => {
|
||||||
if (plugin.type === 'bundle')
|
if (plugin.type === 'bundle')
|
||||||
return `${MARKETPLACE_URL_PREFIX}/bundles/${plugin.org}/${plugin.name}`
|
return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params)
|
||||||
return `${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}`
|
return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => {
|
export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => {
|
||||||
|
@@ -33,8 +33,9 @@ import { useGetLanguage } from '@/context/i18n'
|
|||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
||||||
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
|
import { API_PREFIX } from '@/config'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
const i18nPrefix = 'plugin.action'
|
const i18nPrefix = 'plugin.action'
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ const DetailHeader = ({
|
|||||||
if (isFromGitHub)
|
if (isFromGitHub)
|
||||||
return `https://github.com/${meta!.repo}`
|
return `https://github.com/${meta!.repo}`
|
||||||
if (isFromMarketplace)
|
if (isFromMarketplace)
|
||||||
return `${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`
|
return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })
|
||||||
return ''
|
return ''
|
||||||
}, [author, isFromGitHub, isFromMarketplace, meta, name, theme])
|
}, [author, isFromGitHub, isFromMarketplace, meta, name, theme])
|
||||||
|
|
||||||
|
@@ -21,13 +21,14 @@ import OrgInfo from '../card/base/org-info'
|
|||||||
import Title from '../card/base/title'
|
import Title from '../card/base/title'
|
||||||
import Action from './action'
|
import Action from './action'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
|
import { API_PREFIX } from '@/config'
|
||||||
import { useSingleCategories } from '../hooks'
|
import { useSingleCategories } from '../hooks'
|
||||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { gte } from 'semver'
|
import { gte } from 'semver'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -166,7 +167,7 @@ const PluginItem: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
{source === PluginSource.marketplace
|
{source === PluginSource.marketplace
|
||||||
&& <>
|
&& <>
|
||||||
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='flex items-center gap-0.5'>
|
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='flex items-center gap-0.5'>
|
||||||
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div>
|
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div>
|
||||||
<RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' />
|
<RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' />
|
||||||
</a>
|
</a>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo, useRef, useState } from 'react'
|
'use client'
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||||
import { FileZip } from '@/app/components/base/icons/src/vender/solid/files'
|
import { FileZip } from '@/app/components/base/icons/src/vender/solid/files'
|
||||||
import { Github } from '@/app/components/base/icons/src/vender/solid/general'
|
import { Github } from '@/app/components/base/icons/src/vender/solid/general'
|
||||||
@@ -14,12 +15,18 @@ import { noop } from 'lodash-es'
|
|||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
|
||||||
|
type InstallMethod = {
|
||||||
|
icon: React.FC<{ className?: string }>
|
||||||
|
text: string
|
||||||
|
action: string
|
||||||
|
}
|
||||||
|
|
||||||
const Empty = () => {
|
const Empty = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
|
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -39,6 +46,22 @@ const Empty = () => {
|
|||||||
return t('plugin.list.notFound')
|
return t('plugin.list.notFound')
|
||||||
}, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery])
|
}, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery])
|
||||||
|
|
||||||
|
const [installMethods, setInstallMethods] = useState<InstallMethod[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
const methods = []
|
||||||
|
if (enable_marketplace)
|
||||||
|
methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' })
|
||||||
|
|
||||||
|
if (plugin_installation_permission.restrict_to_marketplace_only) {
|
||||||
|
setInstallMethods(methods)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' })
|
||||||
|
methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' })
|
||||||
|
setInstallMethods(methods)
|
||||||
|
}
|
||||||
|
}, [plugin_installation_permission, enable_marketplace, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative z-0 w-full grow'>
|
<div className='relative z-0 w-full grow'>
|
||||||
{/* skeleton */}
|
{/* skeleton */}
|
||||||
@@ -71,15 +94,7 @@ const Empty = () => {
|
|||||||
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
|
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
|
||||||
/>
|
/>
|
||||||
<div className='flex w-full flex-col gap-y-1'>
|
<div className='flex w-full flex-col gap-y-1'>
|
||||||
{[
|
{installMethods.map(({ icon: Icon, text, action }) => (
|
||||||
...(
|
|
||||||
(enable_marketplace)
|
|
||||||
? [{ icon: MagicBox, text: t('plugin.list.source.marketplace'), action: 'marketplace' }]
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
{ icon: Github, text: t('plugin.list.source.github'), action: 'github' },
|
|
||||||
{ icon: FileZip, text: t('plugin.list.source.local'), action: 'local' },
|
|
||||||
].map(({ icon: Icon, text, action }) => (
|
|
||||||
<Button
|
<Button
|
||||||
key={action}
|
key={action}
|
||||||
className='justify-start gap-x-0.5 px-3'
|
className='justify-start gap-x-0.5 px-3'
|
||||||
|
@@ -136,7 +136,7 @@ const PluginPage = ({
|
|||||||
const options = usePluginPageContext(v => v.options)
|
const options = usePluginPageContext(v => v.options)
|
||||||
const activeTab = usePluginPageContext(v => v.activeTab)
|
const activeTab = usePluginPageContext(v => v.activeTab)
|
||||||
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
|
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
|
||||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
const { enable_marketplace, branding } = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
|
|
||||||
const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab])
|
const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab])
|
||||||
const isExploringMarketplace = useMemo(() => {
|
const isExploringMarketplace = useMemo(() => {
|
||||||
@@ -225,7 +225,7 @@ const PluginPage = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
canSetPermissions && (
|
canSetPermissions && !branding.enabled && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
popupContent={t('plugin.privilege.title')}
|
popupContent={t('plugin.privilege.title')}
|
||||||
>
|
>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
|
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||||
@@ -22,6 +22,13 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
|||||||
type Props = {
|
type Props = {
|
||||||
onSwitchToMarketplaceTab: () => void
|
onSwitchToMarketplaceTab: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstallMethod = {
|
||||||
|
icon: React.FC<{ className?: string }>
|
||||||
|
text: string
|
||||||
|
action: string
|
||||||
|
}
|
||||||
|
|
||||||
const InstallPluginDropdown = ({
|
const InstallPluginDropdown = ({
|
||||||
onSwitchToMarketplaceTab,
|
onSwitchToMarketplaceTab,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -30,7 +37,7 @@ const InstallPluginDropdown = ({
|
|||||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0]
|
||||||
@@ -54,6 +61,22 @@ const InstallPluginDropdown = ({
|
|||||||
// console.log(res)
|
// console.log(res)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
const [installMethods, setInstallMethods] = useState<InstallMethod[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
const methods = []
|
||||||
|
if (enable_marketplace)
|
||||||
|
methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' })
|
||||||
|
|
||||||
|
if (plugin_installation_permission.restrict_to_marketplace_only) {
|
||||||
|
setInstallMethods(methods)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' })
|
||||||
|
methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' })
|
||||||
|
setInstallMethods(methods)
|
||||||
|
}
|
||||||
|
}, [plugin_installation_permission, enable_marketplace, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
open={isMenuOpen}
|
open={isMenuOpen}
|
||||||
@@ -84,15 +107,7 @@ const InstallPluginDropdown = ({
|
|||||||
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
|
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
|
||||||
/>
|
/>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
{[
|
{installMethods.map(({ icon: Icon, text, action }) => (
|
||||||
...(
|
|
||||||
(enable_marketplace)
|
|
||||||
? [{ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }]
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
{ icon: Github, text: t('plugin.source.github'), action: 'github' },
|
|
||||||
{ icon: FileZip, text: t('plugin.source.local'), action: 'local' },
|
|
||||||
].map(({ icon: Icon, text, action }) => (
|
|
||||||
<div
|
<div
|
||||||
key={action}
|
key={action}
|
||||||
className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
|
className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
|
||||||
|
@@ -94,7 +94,11 @@ export type PluginManifestInMarket = {
|
|||||||
introduction: string
|
introduction: string
|
||||||
verified: boolean
|
verified: boolean
|
||||||
install_count: number
|
install_count: number
|
||||||
badges: string[]
|
badges: string[],
|
||||||
|
verification: {
|
||||||
|
authorized_category: 'langgenius' | 'partner' | 'community'
|
||||||
|
},
|
||||||
|
from: Dependency['type']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginDetail = {
|
export type PluginDetail = {
|
||||||
@@ -145,7 +149,11 @@ export type Plugin = {
|
|||||||
settings: CredentialFormSchemaBase[]
|
settings: CredentialFormSchemaBase[]
|
||||||
}
|
}
|
||||||
tags: { name: string }[]
|
tags: { name: string }[]
|
||||||
badges: string[]
|
badges: string[],
|
||||||
|
verification: {
|
||||||
|
authorized_category: 'langgenius' | 'partner' | 'community'
|
||||||
|
},
|
||||||
|
from: Dependency['type']
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PermissionType {
|
export enum PermissionType {
|
||||||
|
@@ -12,7 +12,7 @@ import { useMarketplace } from './hooks'
|
|||||||
import List from '@/app/components/plugins/marketplace/list'
|
import List from '@/app/components/plugins/marketplace/list'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { getLocaleOnClient } from '@/i18n'
|
import { getLocaleOnClient } from '@/i18n'
|
||||||
import { MARKETPLACE_URL_PREFIX } from '@/config'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
type MarketplaceProps = {
|
type MarketplaceProps = {
|
||||||
searchPluginText: string
|
searchPluginText: string
|
||||||
@@ -84,7 +84,7 @@ const Marketplace = ({
|
|||||||
</span>
|
</span>
|
||||||
{t('common.operation.in')}
|
{t('common.operation.in')}
|
||||||
<a
|
<a
|
||||||
href={`${MARKETPLACE_URL_PREFIX}?language=${locale}&q=${searchPluginText}&tags=${filterPluginTags.join(',')}${theme ? `&theme=${theme}` : ''}`}
|
href={getMarketplaceUrl('', { language: locale, q: searchPluginText, tags: filterPluginTags.join(','), theme })}
|
||||||
className='system-sm-medium ml-1 flex items-center text-text-accent'
|
className='system-sm-medium ml-1 flex items-center text-text-accent'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>
|
>
|
||||||
|
@@ -12,9 +12,9 @@ import {
|
|||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { MARKETPLACE_URL_PREFIX } from '@/config'
|
|
||||||
import { useDownloadPlugin } from '@/service/use-plugins'
|
import { useDownloadPlugin } from '@/service/use-plugins'
|
||||||
import { downloadFile } from '@/utils/format'
|
import { downloadFile } from '@/utils/format'
|
||||||
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -80,7 +80,7 @@ const OperationDropdown: FC<Props> = ({
|
|||||||
<PortalToFollowElemContent className='z-[9999]'>
|
<PortalToFollowElemContent className='z-[9999]'>
|
||||||
<div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
<div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||||
<div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div>
|
<div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div>
|
||||||
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
|
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
|
||||||
</div>
|
</div>
|
||||||
</PortalToFollowElemContent>
|
</PortalToFollowElemContent>
|
||||||
</PortalToFollowElem>
|
</PortalToFollowElem>
|
||||||
|
@@ -1,30 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import classNames from '@/utils/classnames'
|
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
||||||
import { useTheme } from 'next-themes'
|
|
||||||
|
|
||||||
type LoginLogoProps = {
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoginLogo: FC<LoginLogoProps> = ({
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
const { systemFeatures } = useGlobalPublicStore()
|
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
|
|
||||||
if (systemFeatures.branding.enabled)
|
|
||||||
src = systemFeatures.branding.login_page_logo
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={src}
|
|
||||||
className={classNames('block w-auto h-10', className)}
|
|
||||||
alt='logo'
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LoginLogo
|
|
@@ -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',
|
||||||
|
@@ -154,6 +154,7 @@ const translation = {
|
|||||||
next: 'Next',
|
next: 'Next',
|
||||||
pluginLoadError: 'Plugin load error',
|
pluginLoadError: 'Plugin load error',
|
||||||
pluginLoadErrorDesc: 'This plugin will not be installed',
|
pluginLoadErrorDesc: 'This plugin will not be installed',
|
||||||
|
installWarning: 'This plugin is not allowed to be installed.',
|
||||||
},
|
},
|
||||||
installFromGitHub: {
|
installFromGitHub: {
|
||||||
installPlugin: 'Install plugin from GitHub',
|
installPlugin: 'Install plugin from GitHub',
|
||||||
|
@@ -64,6 +64,8 @@ const translation = {
|
|||||||
in: '中',
|
in: '中',
|
||||||
format: 'フォーマット',
|
format: 'フォーマット',
|
||||||
more: 'もっと',
|
more: 'もっと',
|
||||||
|
selectAll: 'すべて選択',
|
||||||
|
deSelectAll: 'すべて選択解除',
|
||||||
},
|
},
|
||||||
errorMsg: {
|
errorMsg: {
|
||||||
fieldRequired: '{{field}}は必要です',
|
fieldRequired: '{{field}}は必要です',
|
||||||
|
@@ -137,6 +137,7 @@ const translation = {
|
|||||||
installPlugin: 'プラグインをインストールする',
|
installPlugin: 'プラグインをインストールする',
|
||||||
back: '戻る',
|
back: '戻る',
|
||||||
uploadingPackage: '{{packageName}}をアップロード中...',
|
uploadingPackage: '{{packageName}}をアップロード中...',
|
||||||
|
installWarning: 'このプラグインはインストールを許可されていません。',
|
||||||
},
|
},
|
||||||
installFromGitHub: {
|
installFromGitHub: {
|
||||||
installedSuccessfully: 'インストールに成功しました',
|
installedSuccessfully: 'インストールに成功しました',
|
||||||
|
@@ -64,6 +64,8 @@ const translation = {
|
|||||||
skip: '跳过',
|
skip: '跳过',
|
||||||
format: '格式化',
|
format: '格式化',
|
||||||
more: '更多',
|
more: '更多',
|
||||||
|
selectAll: '全选',
|
||||||
|
deSelectAll: '取消全选',
|
||||||
},
|
},
|
||||||
errorMsg: {
|
errorMsg: {
|
||||||
fieldRequired: '{{field}} 为必填项',
|
fieldRequired: '{{field}} 为必填项',
|
||||||
|
@@ -154,6 +154,7 @@ const translation = {
|
|||||||
next: '下一步',
|
next: '下一步',
|
||||||
pluginLoadError: '插件加载错误',
|
pluginLoadError: '插件加载错误',
|
||||||
pluginLoadErrorDesc: '此插件将不会被安装',
|
pluginLoadErrorDesc: '此插件将不会被安装',
|
||||||
|
installWarning: '此插件不允许安装。',
|
||||||
},
|
},
|
||||||
installFromGitHub: {
|
installFromGitHub: {
|
||||||
installPlugin: '从 GitHub 安装插件',
|
installPlugin: '从 GitHub 安装插件',
|
||||||
|
@@ -13,12 +13,23 @@ export enum LicenseStatus {
|
|||||||
LOST = 'lost',
|
LOST = 'lost',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum InstallationScope {
|
||||||
|
ALL = 'all',
|
||||||
|
NONE = 'none',
|
||||||
|
OFFICIAL_ONLY = 'official_only',
|
||||||
|
OFFICIAL_AND_PARTNER = 'official_and_specific_partners',
|
||||||
|
}
|
||||||
|
|
||||||
type License = {
|
type License = {
|
||||||
status: LicenseStatus
|
status: LicenseStatus
|
||||||
expired_at: string | null
|
expired_at: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SystemFeatures = {
|
export type SystemFeatures = {
|
||||||
|
plugin_installation_permission: {
|
||||||
|
plugin_installation_scope: InstallationScope,
|
||||||
|
restrict_to_marketplace_only: boolean
|
||||||
|
},
|
||||||
sso_enforced_for_signin: boolean
|
sso_enforced_for_signin: boolean
|
||||||
sso_enforced_for_signin_protocol: SSOProtocol | ''
|
sso_enforced_for_signin_protocol: SSOProtocol | ''
|
||||||
sso_enforced_for_web: boolean
|
sso_enforced_for_web: boolean
|
||||||
@@ -50,6 +61,10 @@ export type SystemFeatures = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSystemFeatures: SystemFeatures = {
|
export const defaultSystemFeatures: SystemFeatures = {
|
||||||
|
plugin_installation_permission: {
|
||||||
|
plugin_installation_scope: InstallationScope.ALL,
|
||||||
|
restrict_to_marketplace_only: false,
|
||||||
|
},
|
||||||
sso_enforced_for_signin: false,
|
sso_enforced_for_signin: false,
|
||||||
sso_enforced_for_signin_protocol: '',
|
sso_enforced_for_signin_protocol: '',
|
||||||
sso_enforced_for_web: false,
|
sso_enforced_for_web: false,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config'
|
import { MARKETPLACE_URL_PREFIX, MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config'
|
||||||
import {
|
import {
|
||||||
CONTEXT_PLACEHOLDER_TEXT,
|
CONTEXT_PLACEHOLDER_TEXT,
|
||||||
HISTORY_PLACEHOLDER_TEXT,
|
HISTORY_PLACEHOLDER_TEXT,
|
||||||
@@ -108,3 +108,15 @@ export const getVars = (value: string) => {
|
|||||||
// Set the value of basePath
|
// Set the value of basePath
|
||||||
// example: /dify
|
// example: /dify
|
||||||
export const basePath = ''
|
export const basePath = ''
|
||||||
|
|
||||||
|
export function getMarketplaceUrl(path: string, params?: Record<string, string | undefined>) {
|
||||||
|
const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) })
|
||||||
|
if (params) {
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
const value = params[key]
|
||||||
|
if (value !== undefined && value !== null)
|
||||||
|
searchParams.append(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return `${MARKETPLACE_URL_PREFIX}${path}?${searchParams.toString()}`
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user