This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,272 @@
'use client'
import { useImperativeHandle } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
import MarketplaceItem from '../item/marketplace-item'
import GithubItem from '../item/github-item'
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { produce } from 'immer'
import PackageItem from '../item/package-item'
import LoadingError from '../../base/loading-error'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
type Props = {
allPlugins: Dependency[]
selectedPlugins: Plugin[]
onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void
onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void
onDeSelectAll: () => void
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
isFromMarketPlace?: boolean
ref?: React.Ref<ExposeRefs>
}
export type ExposeRefs = {
selectAllPlugins: () => void
deSelectAllPlugins: () => void
}
const InstallByDSLList = ({
allPlugins,
selectedPlugins,
onSelect,
onSelectAll,
onDeSelectAll,
onLoadedAllPlugin,
isFromMarketPlace,
ref,
}: Props) => {
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
// split org, name, version by / and :
// and remove @ and its suffix
const [orgPart, nameAndVersionPart] = dependecy.marketplace_plugin_unique_identifier!.split('@')[0].split('/')
const [name, version] = nameAndVersionPart.split(':')
return {
organization: orgPart,
plugin: name,
version,
}
}))
// has meta(org,name,version), to get id
const { isLoading: isFetchingDataByMeta, data: infoByMeta, error: infoByMetaError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!))
const [plugins, doSetPlugins] = useState<(Plugin | undefined)[]>((() => {
const hasLocalPackage = allPlugins.some(d => d.type === 'package')
if (!hasLocalPackage)
return []
const _plugins = allPlugins.map((d) => {
if (d.type === 'package') {
return {
...(d as any).value.manifest,
plugin_id: (d as any).value.unique_identifier,
}
}
return undefined
})
return _plugins
})())
const pluginsRef = React.useRef<(Plugin | undefined)[]>(plugins)
const setPlugins = useCallback((p: (Plugin | undefined)[]) => {
doSetPlugins(p)
pluginsRef.current = p
}, [])
const [errorIndexes, setErrorIndexes] = useState<number[]>([])
const handleGitHubPluginFetched = useCallback((index: number) => {
return (p: Plugin) => {
const nextPlugins = produce(pluginsRef.current, (draft) => {
draft[index] = p
})
setPlugins(nextPlugins)
}
}, [setPlugins])
const handleGitHubPluginFetchError = useCallback((index: number) => {
return () => {
setErrorIndexes([...errorIndexes, index])
}
}, [errorIndexes])
const marketPlaceInDSLIndex = useMemo(() => {
const res: number[] = []
allPlugins.forEach((d, index) => {
if (d.type === 'marketplace')
res.push(index)
})
return res
}, [allPlugins])
useEffect(() => {
if (!isFetchingMarketplaceDataById && infoGetById?.data.list) {
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]
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[] = []
const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => {
if (payloads[i]) {
draft[index] = {
...payloads[i],
version: payloads[i]!.version || payloads[i]!.latest_version,
}
}
else { failedIndex.push(index) }
})
})
setPlugins(nextPlugins)
if (failedIndex.length > 0)
setErrorIndexes([...errorIndexes, ...failedIndex])
}
}, [isFetchingMarketplaceDataById])
useEffect(() => {
if (!isFetchingDataByMeta && infoByMeta?.data.list) {
const payloads = infoByMeta?.data.list
const failedIndex: number[] = []
const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => {
if (payloads[i]) {
const item = payloads[i]
draft[index] = {
...item.plugin,
plugin_id: item.version.unique_identifier,
}
}
else {
failedIndex.push(index)
}
})
})
setPlugins(nextPlugins)
if (failedIndex.length > 0)
setErrorIndexes([...errorIndexes, ...failedIndex])
}
}, [isFetchingDataByMeta])
useEffect(() => {
// get info all failed
if (infoByMetaError || infoByIdError)
setErrorIndexes([...errorIndexes, ...marketPlaceInDSLIndex])
}, [infoByMetaError, infoByIdError])
const isLoadedAllData = (plugins.filter(p => !!p).length + errorIndexes.length) === allPlugins.length
const { installedInfo } = useCheckInstalled({
pluginIds: plugins?.filter(p => !!p).map((d) => {
return `${d?.org || d?.author}/${d?.name}`
}) || [],
enabled: isLoadedAllData,
})
const getVersionInfo = useCallback((pluginId: string) => {
const pluginDetail = installedInfo?.[pluginId]
const hasInstalled = !!pluginDetail
return {
hasInstalled,
installedVersion: pluginDetail?.installedVersion,
toInstallVersion: '',
}
}, [installedInfo])
useEffect(() => {
if (isLoadedAllData && installedInfo)
onLoadedAllPlugin(installedInfo!)
}, [isLoadedAllData, installedInfo])
const handleSelect = useCallback((index: number) => {
return () => {
const canSelectPlugins = plugins.filter((p) => {
const { canInstall } = pluginInstallLimit(p!, systemFeatures)
return canInstall
})
onSelect(plugins[index]!, index, canSelectPlugins.length)
}
}, [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) => {
if (errorIndexes.includes(index)) {
return (
<LoadingError key={index} />
)
}
const plugin = plugins[index]
if (d.type === 'github') {
return (<GithubItem
key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)}
dependency={d as GitHubItemAndMarketPlaceDependency}
onFetchedPayload={handleGitHubPluginFetched(index)}
onFetchError={handleGitHubPluginFetchError(index)}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/>)
}
if (d.type === 'marketplace') {
return (
<MarketplaceItem
key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)}
payload={{ ...plugin, from: d.type } as Plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/>
)
}
// Local package
return (
<PackageItem
key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)}
payload={d as PackageDependency}
isFromMarketPlace={isFromMarketPlace}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/>
)
})
}
</>
)
}
export default InstallByDSLList

View File

@@ -0,0 +1,223 @@
'use client'
import type { FC } from 'react'
import { useRef } from 'react'
import React, { useCallback, useState } from 'react'
import {
type Dependency,
type InstallStatus,
type InstallStatusResponse,
type Plugin,
TaskStatus,
type 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, usePluginTaskList } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting'
import { useMittContextSelector } from '@/context/mitt-context'
import Checkbox from '@/app/components/base/checkbox'
import checkTaskStatus from '../../base/check-task-status'
const i18nPrefix = 'plugin.installModal'
type Props = {
allPlugins: Dependency[]
onStartToInstall?: () => void
onInstalled: (plugins: Plugin[], installStatus: InstallStatus[]) => void
onCancel: () => void
isFromMarketPlace?: boolean
isHideButton?: boolean
}
const Install: FC<Props> = ({
allPlugins,
onStartToInstall,
onInstalled,
onCancel,
isFromMarketPlace,
isHideButton,
}) => {
const { t } = useTranslation()
const emit = useMittContextSelector(s => s.emit)
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length
const installMultiRef = useRef<ExposeRefs>(null)
const { refreshPluginList } = useRefreshPluginList()
const [isSelectAll, setIsSelectAll] = useState(false)
const handleClickSelectAll = useCallback(() => {
if (isSelectAll)
installMultiRef.current?.deSelectAllPlugins()
else
installMultiRef.current?.selectAllPlugins()
}, [isSelectAll])
const [canInstall, setCanInstall] = React.useState(false)
const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined)
const handleLoadedAllPlugin = useCallback((installedInfo: Record<string, VersionInfo> | undefined) => {
handleClickSelectAll()
setInstalledInfo(installedInfo)
setCanInstall(true)
}, [])
const {
check,
stop,
} = checkTaskStatus()
const handleCancel = useCallback(() => {
stop()
onCancel()
}, [onCancel, stop])
const { handleRefetch } = usePluginTaskList()
// Install from marketplace and github
const { mutate: installOrUpdate, isPending: isInstalling } = useInstallOrUpdate({
onSuccess: async (res: InstallStatusResponse[]) => {
const isAllSettled = res.every(r => r.status === TaskStatus.success || r.status === TaskStatus.failed)
// if all settled, return the install status
if (isAllSettled) {
onInstalled(selectedPlugins, res.map((r, i) => {
return ({
success: r.status === TaskStatus.success,
isFromMarketPlace: allPlugins[selectedIndexes[i]].type === 'marketplace',
})
}))
const hasInstallSuccess = res.some(r => r.status === TaskStatus.success)
if (hasInstallSuccess) {
refreshPluginList(undefined, true)
emit('plugin:install:success', selectedPlugins.map((p) => {
return `${p.plugin_id}/${p.name}`
}))
}
return
}
// if not all settled, keep checking the status of the plugins
handleRefetch()
const installStatus = await Promise.all(res.map(async (item, index) => {
if (item.status !== TaskStatus.running) {
return {
success: item.status === TaskStatus.success,
isFromMarketPlace: allPlugins[selectedIndexes[index]].type === 'marketplace',
}
}
const { status } = await check({
taskId: item.taskId,
pluginUniqueIdentifier: item.uniqueIdentifier,
})
return {
success: status === TaskStatus.success,
isFromMarketPlace: allPlugins[selectedIndexes[index]].type === 'marketplace',
}
}))
onInstalled(selectedPlugins, installStatus)
const hasInstallSuccess = installStatus.some(r => r.success)
if (hasInstallSuccess) {
emit('plugin:install:success', selectedPlugins.map((p) => {
return `${p.plugin_id}/${p.name}`
}))
}
},
})
const handleInstall = () => {
onStartToInstall?.()
installOrUpdate({
payload: allPlugins.filter((_d, index) => selectedIndexes.includes(index)),
plugin: selectedPlugins,
installedInfo: installedInfo!,
})
}
const [isIndeterminate, setIsIndeterminate] = useState(false)
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 (
<>
<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'>
<p>{t(`${i18nPrefix}.${selectedPluginsNum > 1 ? 'readyToInstallPackages' : 'readyToInstallPackage'}`, { num: selectedPluginsNum })}</p>
</div>
<div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'>
<InstallMulti
ref={installMultiRef}
allPlugins={allPlugins}
selectedPlugins={selectedPlugins}
onSelect={handleSelect}
onSelectAll={handleSelectAll}
onDeSelectAll={handleDeSelectAll}
onLoadedAllPlugin={handleLoadedAllPlugin}
isFromMarketPlace={isFromMarketPlace}
/>
</div>
</div>
{/* Action Buttons */}
{!isHideButton && (
<div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'>
<div className='px-2'>
{canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
<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={handleCancel}>
{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>
</div>
</div>
)}
</>
)
}
export default React.memo(Install)

View File

@@ -0,0 +1,65 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { InstallStatus, Plugin } from '../../../types'
import Card from '@/app/components/plugins/card'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import useGetIcon from '../../base/use-get-icon'
import { MARKETPLACE_API_PREFIX } from '@/config'
type Props = {
list: Plugin[]
installStatus: InstallStatus[]
onCancel: () => void
isHideButton?: boolean
}
const Installed: FC<Props> = ({
list,
installStatus,
onCancel,
isHideButton,
}) => {
const { t } = useTranslation()
const { getIconUrl } = useGetIcon()
return (
<>
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
{/* <p className='text-text-secondary system-md-regular'>{(isFailed && errMsg) ? errMsg : t(`plugin.installModal.${isFailed ? 'installFailedDesc' : 'installedSuccessfullyDesc'}`)}</p> */}
<div className='flex flex-wrap content-start items-start gap-1 space-y-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
{list.map((plugin, index) => {
return (
<Card
key={plugin.plugin_id}
className='w-full'
payload={{
...plugin,
icon: installStatus[index].isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon` : getIconUrl(plugin.icon),
}}
installed={installStatus[index].success}
installFailed={!installStatus[index].success}
titleLeft={plugin.version ? <Badge className='mx-1' size="s" state={BadgeState.Default}>{plugin.version}</Badge> : null}
/>
)
})}
</div>
</div>
{/* Action Buttons */}
{!isHideButton && (
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
<Button
variant='primary'
className='min-w-[72px]'
onClick={onCancel}
>
{t('common.operation.close')}
</Button>
</div>
)}
</>
)
}
export default React.memo(Installed)