dify/web/app/components/plugins/install-plugin/install-bundle/steps/hooks/use-install-multi-state.ts
Stephen Zhou eb71e47f3b
chore: migrate linting to vp lint with ESLint fallback (#38834)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-13 06:41:53 +00:00

293 lines
9.4 KiB
TypeScript

'use client'
import type {
Dependency,
GitHubItemAndMarketPlaceDependency,
PackageDependency,
Plugin,
VersionInfo,
} from '@/app/components/plugins/types'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { pluginInstallLimit } from '@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit'
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
type UseInstallMultiStateParams = {
allPlugins: Dependency[]
selectedPlugins: Plugin[]
onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
}
type MarketplacePluginInfo = {
organization: string
plugin: string
version?: string
}
type MarketplaceRequest = {
dslIndex: number
dependency: GitHubItemAndMarketPlaceDependency
info: MarketplacePluginInfo
}
export function getPluginKey(plugin: Plugin | undefined): string {
return `${plugin?.org || plugin?.author}/${plugin?.name}`
}
function parseMarketplaceIdentifier(identifier?: string): MarketplacePluginInfo | null {
if (!identifier) return null
const withoutHash = identifier.split('@')[0]
const [organization, nameAndVersionPart] = withoutHash!.split('/')
if (!organization || !nameAndVersionPart) return null
const [plugin, version] = nameAndVersionPart.split(':')
if (!plugin) return null
return { organization, plugin, version }
}
function getMarketplacePluginInfo(
value: GitHubItemAndMarketPlaceDependency['value'],
): MarketplacePluginInfo | null {
const parsedInfo = parseMarketplaceIdentifier(
value.marketplace_plugin_unique_identifier || value.plugin_unique_identifier,
)
if (parsedInfo) return parsedInfo
if (!value.organization || !value.plugin) return null
return {
organization: value.organization,
plugin: value.plugin,
version: value.version,
}
}
function initPluginsFromDependencies(allPlugins: Dependency[]): (Plugin | undefined)[] {
if (!allPlugins.some((d) => d.type === 'package')) return []
return allPlugins.map((d) => {
if (d.type !== 'package') return undefined
const { manifest, unique_identifier } = (d as PackageDependency).value
return {
...manifest,
plugin_id: unique_identifier,
} as unknown as Plugin
})
}
export function useInstallMultiState({
allPlugins,
selectedPlugins,
onSelect,
onLoadedAllPlugin,
}: UseInstallMultiStateParams) {
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
// Marketplace plugins filtering and index mapping
const marketplacePlugins = useMemo(
() =>
allPlugins.filter((d): d is GitHubItemAndMarketPlaceDependency => d.type === 'marketplace'),
[allPlugins],
)
const marketPlaceInDSLIndex = useMemo(() => {
return allPlugins.reduce<number[]>((acc, d, index) => {
if (d.type === 'marketplace') acc.push(index)
return acc
}, [])
}, [allPlugins])
const { marketplaceRequests, invalidMarketplaceIndexes } = useMemo(() => {
return marketplacePlugins.reduce<{
marketplaceRequests: MarketplaceRequest[]
invalidMarketplaceIndexes: number[]
}>(
(acc, dependency, marketplaceIndex) => {
const dslIndex = marketPlaceInDSLIndex[marketplaceIndex]
if (dslIndex === undefined) return acc
const marketplaceInfo = getMarketplacePluginInfo(dependency.value)
if (!marketplaceInfo) acc.invalidMarketplaceIndexes.push(dslIndex)
else acc.marketplaceRequests.push({ dslIndex, dependency, info: marketplaceInfo })
return acc
},
{
marketplaceRequests: [],
invalidMarketplaceIndexes: [],
},
)
}, [marketPlaceInDSLIndex, marketplacePlugins])
// Marketplace data fetching: by normalized marketplace info
const {
isLoading: isFetchingById,
data: infoGetById,
error: infoByIdError,
} = useFetchPluginsInMarketPlaceByInfo(marketplaceRequests.map((request) => request.info))
// Derive marketplace plugin data and errors from API responses
const { marketplacePluginMap, marketplaceErrorIndexes } = useMemo(() => {
const pluginMap = new Map<number, Plugin>()
const errorSet = new Set<number>(invalidMarketplaceIndexes)
// Process "by ID" response
if (!isFetchingById && infoGetById?.data.list) {
const payloads = infoGetById.data.list
const pluginById = new Map(payloads.map((item) => [item.plugin.plugin_id, item.plugin]))
marketplaceRequests.forEach((request, requestIndex) => {
const pluginId = (
request.dependency.value.marketplace_plugin_unique_identifier ||
request.dependency.value.plugin_unique_identifier
)?.split(':')[0]
const pluginInfo =
(pluginId ? pluginById.get(pluginId) : undefined) || payloads[requestIndex]?.plugin
if (pluginInfo) {
pluginMap.set(
request.dslIndex,
getFormattedPlugin({
...pluginInfo,
from: request.dependency.type,
version: pluginInfo.version || pluginInfo.latest_version,
}),
)
} else {
errorSet.add(request.dslIndex)
}
})
}
// Mark all marketplace indexes as errors on fetch failure
if (infoByIdError) marketPlaceInDSLIndex.forEach((index) => errorSet.add(index))
return { marketplacePluginMap: pluginMap, marketplaceErrorIndexes: errorSet }
}, [
invalidMarketplaceIndexes,
isFetchingById,
infoGetById,
infoByIdError,
marketPlaceInDSLIndex,
marketplaceRequests,
])
// GitHub-fetched plugins and errors (imperative state from child callbacks)
const [githubPluginMap, setGithubPluginMap] = useState<Map<number, Plugin>>(() => new Map())
const [githubErrorIndexes, setGithubErrorIndexes] = useState<number[]>([])
// Merge all plugin sources into a single array
const plugins = useMemo(() => {
const initial = initPluginsFromDependencies(allPlugins)
const result: (Plugin | undefined)[] = allPlugins.map((_, i) => initial[i])
marketplacePluginMap.forEach((plugin, index) => {
result[index] = plugin
})
githubPluginMap.forEach((plugin, index) => {
result[index] = plugin
})
return result
}, [allPlugins, marketplacePluginMap, githubPluginMap])
// Merge all error sources
const errorIndexes = useMemo(() => {
return [...marketplaceErrorIndexes, ...githubErrorIndexes]
}, [marketplaceErrorIndexes, githubErrorIndexes])
// Check installed status after all data is loaded
const isLoadedAllData = plugins.filter(Boolean).length + errorIndexes.length === allPlugins.length
const { installedInfo } = useCheckInstalled({
pluginIds: plugins.filter(Boolean).map((d) => getPluginKey(d)) || [],
enabled: isLoadedAllData,
})
// Notify parent when all plugin data and install info is ready
useEffect(() => {
if (isLoadedAllData && installedInfo) onLoadedAllPlugin(installedInfo!)
// oxlint-disable-next-line react/exhaustive-deps
}, [isLoadedAllData, installedInfo])
// Callback: handle GitHub plugin fetch success
const handleGitHubPluginFetched = useCallback((index: number) => {
return (p: Plugin) => {
setGithubPluginMap((prev) => new Map(prev).set(index, p))
}
}, [])
// Callback: handle GitHub plugin fetch error
const handleGitHubPluginFetchError = useCallback((index: number) => {
return () => {
setGithubErrorIndexes((prev) => [...prev, index])
}
}, [])
// Callback: get version info for a plugin by its key
const getVersionInfo = useCallback(
(pluginId: string) => {
const pluginDetail = installedInfo?.[pluginId]
return {
hasInstalled: !!pluginDetail,
installedVersion: pluginDetail?.installedVersion,
toInstallVersion: '',
}
},
[installedInfo],
)
// Callback: handle plugin selection
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],
)
// Callback: check if a plugin at given index is selected
const isPluginSelected = useCallback(
(index: number) => {
return !!selectedPlugins.find((p) => p.plugin_id === plugins[index]?.plugin_id)
},
[selectedPlugins, plugins],
)
// Callback: get all installable plugins with their indexes
const getInstallablePlugins = useCallback(() => {
const selectedIndexes: number[] = []
const installablePlugins: Plugin[] = []
allPlugins.forEach((_d, index) => {
const p = plugins[index]
if (!p) return
const { canInstall } = pluginInstallLimit(p, systemFeatures)
if (canInstall) {
selectedIndexes.push(index)
installablePlugins.push(p)
}
})
return { selectedIndexes, installablePlugins }
}, [allPlugins, plugins, systemFeatures])
return {
plugins,
errorIndexes,
handleGitHubPluginFetched,
handleGitHubPluginFetchError,
getVersionInfo,
handleSelect,
isPluginSelected,
getInstallablePlugins,
}
}