Merge branch 'feat/plugins' of https://github.com/langgenius/dify into feat/plugins

This commit is contained in:
twwu 2024-10-31 16:20:31 +08:00
commit 7d4f8e0082
9 changed files with 147 additions and 29 deletions

View File

@ -9,7 +9,7 @@ const Line = ({
<path d="M1 0.5L1 240.5" stroke="url(#paint0_linear_1989_74474)"/> <path d="M1 0.5L1 240.5" stroke="url(#paint0_linear_1989_74474)"/>
<defs> <defs>
<linearGradient id="paint0_linear_1989_74474" x1="-7.99584" y1="240.5" x2="-7.88094" y2="0.50004" gradientUnits="userSpaceOnUse"> <linearGradient id="paint0_linear_1989_74474" x1="-7.99584" y1="240.5" x2="-7.88094" y2="0.50004" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stopOpacity="0.01"/> <stop stopColor="white" stopOpacity="0.01"/>
<stop offset="0.503965" stopColor="#101828" stopOpacity="0.08"/> <stop offset="0.503965" stopColor="#101828" stopOpacity="0.08"/>
<stop offset="1" stopColor="white" stopOpacity="0.01"/> <stop offset="1" stopColor="white" stopOpacity="0.01"/>
</linearGradient> </linearGradient>

View File

@ -3,28 +3,68 @@ import {
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
import { useDebounceFn } from 'ahooks'
import type { Plugin } from '@/app/components/plugins/types' import type { Plugin } from '@/app/components/plugins/types'
import type { MarketplaceCollection } from '@/app/components/plugins/marketplace/types' import type {
import { getMarketplaceCollectionsAndPlugins } from '@/app/components/plugins/marketplace/utils' MarketplaceCollection,
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
import {
getMarketplaceCollectionsAndPlugins,
getMarketplacePlugins,
} from '@/app/components/plugins/marketplace/utils'
export const useMarketplace = () => { export const useMarketplace = (searchPluginText: string, filterPluginTags: string[]) => {
const [marketplaceCollections, setMarketplaceCollections] = useState<MarketplaceCollection[]>([]) const [marketplaceCollections, setMarketplaceCollections] = useState<MarketplaceCollection[]>([])
const [marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap] = useState<Record<string, Plugin[]>>({}) const [marketplaceCollectionPluginsMap, setMarketplaceCollectionPluginsMap] = useState<Record<string, Plugin[]>>({})
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const getMarketplaceCollections = useCallback(async () => { const [plugins, setPlugins] = useState<Plugin[]>()
const handleUpldateMarketplaceCollections = useCallback(async () => {
setIsLoading(true) setIsLoading(true)
const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins() const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins()
setIsLoading(false) setIsLoading(false)
setMarketplaceCollections(marketplaceCollections) setMarketplaceCollections(marketplaceCollections)
setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap) setMarketplaceCollectionPluginsMap(marketplaceCollectionPluginsMap)
setPlugins(undefined)
}, []) }, [])
const handleUpdatePlugins = async (query: PluginsSearchParams) => {
setIsLoading(true)
const { marketplacePlugins } = await getMarketplacePlugins(query)
setIsLoading(false)
setPlugins(marketplacePlugins)
}
const { run: handleUpdatePluginsWithDebounced } = useDebounceFn(handleUpdatePlugins, {
wait: 500,
})
useEffect(() => { useEffect(() => {
getMarketplaceCollections() if (searchPluginText || filterPluginTags.length) {
}, [getMarketplaceCollections]) if (searchPluginText) {
handleUpdatePluginsWithDebounced({
query: searchPluginText,
tags: filterPluginTags,
})
return
}
handleUpdatePlugins({
query: searchPluginText,
tags: filterPluginTags,
})
}
else {
handleUpldateMarketplaceCollections()
}
}, [searchPluginText, filterPluginTags, handleUpdatePluginsWithDebounced, handleUpldateMarketplaceCollections])
return { return {
isLoading, isLoading,
marketplaceCollections, marketplaceCollections,
marketplaceCollectionPluginsMap, marketplaceCollectionPluginsMap,
plugins,
} }
} }

View File

@ -4,16 +4,21 @@ import List from '@/app/components/plugins/marketplace/list'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
type MarketplaceProps = { type MarketplaceProps = {
searchPluginText: string
filterPluginTags: string[]
onMarketplaceScroll: () => void onMarketplaceScroll: () => void
} }
const Marketplace = ({ const Marketplace = ({
searchPluginText,
filterPluginTags,
onMarketplaceScroll, onMarketplaceScroll,
}: MarketplaceProps) => { }: MarketplaceProps) => {
const { const {
isLoading, isLoading,
marketplaceCollections, marketplaceCollections,
marketplaceCollectionPluginsMap, marketplaceCollectionPluginsMap,
} = useMarketplace() plugins,
} = useMarketplace(searchPluginText, filterPluginTags)
return ( return (
<div className='shrink-0 sticky -bottom-[442px] h-[530px] overflow-y-auto px-12 py-2 pt-0 bg-background-default-subtle'> <div className='shrink-0 sticky -bottom-[442px] h-[530px] overflow-y-auto px-12 py-2 pt-0 bg-background-default-subtle'>
@ -55,6 +60,7 @@ const Marketplace = ({
<List <List
marketplaceCollections={marketplaceCollections} marketplaceCollections={marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap} marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
plugins={plugins}
showInstallButton showInstallButton
/> />
) )

View File

@ -126,9 +126,13 @@ const ProviderList = () => {
</div> </div>
{ {
enable_marketplace && ( enable_marketplace && (
<Marketplace onMarketplaceScroll={() => { <Marketplace
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) onMarketplaceScroll={() => {
}} /> containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}}
searchPluginText={keywords}
filterPluginTags={tagFilterValue}
/>
) )
} }
</div> </div>

View File

@ -3,6 +3,8 @@ import type { FC, RefObject } from 'react'
import type { ToolWithProvider } from '../types' import type { ToolWithProvider } from '../types'
import { CollectionType } from '../../tools/types' import { CollectionType } from '../../tools/types'
export const CUSTOM_GROUP_NAME = '@@@custom@@@'
export const WORKFLOW_GROUP_NAME = '@@@workflow@@@'
/* /*
{ {
A: { A: {
@ -42,9 +44,9 @@ export const groupItems = (items: ToolWithProvider[], getFirstChar: (item: ToolW
if (item.type === CollectionType.builtIn) if (item.type === CollectionType.builtIn)
groupName = item.author groupName = item.author
else if (item.type === CollectionType.custom) else if (item.type === CollectionType.custom)
groupName = 'custom' groupName = CUSTOM_GROUP_NAME
else else
groupName = 'workflow' groupName = WORKFLOW_GROUP_NAME
if (!acc[letter][groupName]) if (!acc[letter][groupName])
acc[letter][groupName] = [] acc[letter][groupName] = []
@ -76,7 +78,7 @@ const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs }) => {
element.scrollIntoView({ behavior: 'smooth' }) element.scrollIntoView({ behavior: 'smooth' })
} }
return ( return (
<div className="index-bar absolute right-4 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary "> <div className="index-bar absolute right-0 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary ">
<div className='absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]'></div> <div className='absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]'></div>
{letters.map(letter => ( {letters.map(letter => (
<div className="hover:text-text-secondary cursor-pointer" key={letter} onClick={() => handleIndexClick(letter)}> <div className="hover:text-text-secondary cursor-pointer" key={letter} onClick={() => handleIndexClick(letter)}>

View File

@ -1,16 +1,36 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import type { ToolWithProvider } from '../../../types'
import type { BlockEnum } from '../../../types'
import type { ToolDefaultValue } from '../../types'
import Tool from '../tool'
import { ViewType } from '../../view-type-select'
type Props = { type Props = {
payload: ToolWithProvider[]
isShowLetterIndex: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
} }
const ToolViewFlatView: FC<Props> = () => { const ToolViewFlatView: FC<Props> = ({
payload,
isShowLetterIndex,
onSelect,
}) => {
return ( return (
<div> <div>
list... {payload.map(tool => (
<Tool
key={tool.id}
payload={tool}
viewType={ViewType.flat}
isShowLetterIndex={isShowLetterIndex}
onSelect={onSelect}
/>
))}
</div> </div>
) )
} }
export default React.memo(ToolViewFlatView) export default React.memo(ToolViewFlatView)

View File

@ -1,20 +1,33 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useCallback } from 'react'
import type { ToolWithProvider } from '../../../types' import type { ToolWithProvider } from '../../../types'
import type { BlockEnum } from '../../../types' import type { BlockEnum } from '../../../types'
import type { ToolDefaultValue } from '../../types' import type { ToolDefaultValue } from '../../types'
import Item from './item' import Item from './item'
import { useTranslation } from 'react-i18next'
import { CUSTOM_GROUP_NAME, WORKFLOW_GROUP_NAME } from '../../index-bar'
type Props = { type Props = {
payload: Record<string, ToolWithProvider[]> payload: Record<string, ToolWithProvider[]>
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
} }
const OrgTools: FC<Props> = ({ const ToolListTreeView: FC<Props> = ({
payload, payload,
onSelect, onSelect,
}) => { }) => {
const { t } = useTranslation()
const getI18nGroupName = useCallback((name: string) => {
if (name === CUSTOM_GROUP_NAME)
return t('workflow.tabs.customTool')
if (name === WORKFLOW_GROUP_NAME)
return t('workflow.tabs.workflowTool')
return name
}, [t])
if (!payload) return null if (!payload) return null
return ( return (
@ -22,7 +35,7 @@ const OrgTools: FC<Props> = ({
{Object.keys(payload).map(groupName => ( {Object.keys(payload).map(groupName => (
<Item <Item
key={groupName} key={groupName}
groupName={groupName} groupName={getI18nGroupName(groupName)}
toolList={payload[groupName]} toolList={payload[groupName]}
onSelect={onSelect} onSelect={onSelect}
/> />
@ -30,4 +43,5 @@ const OrgTools: FC<Props> = ({
</div> </div>
) )
} }
export default React.memo(OrgTools)
export default React.memo(ToolListTreeView)

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useMemo } from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useGetLanguage } from '@/context/i18n' import { useGetLanguage } from '@/context/i18n'
@ -13,6 +13,7 @@ import ActonItem from './action-item'
import BlockIcon from '../../block-icon' import BlockIcon from '../../block-icon'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
type Props = { type Props = {
className?: string className?: string
@ -29,8 +30,9 @@ const Tool: FC<Props> = ({
isShowLetterIndex, isShowLetterIndex,
onSelect, onSelect,
}) => { }) => {
const { t } = useTranslation()
const language = useGetLanguage() const language = useGetLanguage()
const isTreeView = viewType === ViewType.tree const isFlatView = viewType === ViewType.flat
const actions = payload.tools const actions = payload.tools
const hasAction = payload.type === CollectionType.builtIn const hasAction = payload.type === CollectionType.builtIn
const [isFold, { const [isFold, {
@ -38,10 +40,23 @@ const Tool: FC<Props> = ({
}] = useBoolean(false) }] = useBoolean(false)
const FoldIcon = isFold ? RiArrowDownSLine : RiArrowRightSLine const FoldIcon = isFold ? RiArrowDownSLine : RiArrowRightSLine
const groupName = useMemo(() => {
if (payload.type === CollectionType.builtIn)
return payload.author
if (payload.type === CollectionType.custom)
return t('workflow.tabs.customTool')
if (payload.type === CollectionType.workflow)
return t('workflow.tabs.workflowTool')
return ''
}, [payload.author, payload.type, t])
return ( return (
<div <div
key={payload.id} key={payload.id}
className='mb-1 last-of-type:mb-0' className={cn('mb-1 last-of-type:mb-0', isShowLetterIndex && 'mr-6')}
> >
<div className={cn(className)}> <div className={cn(className)}>
<div <div
@ -69,16 +84,21 @@ const Tool: FC<Props> = ({
/> />
<div className='ml-2 text-sm text-gray-900 flex-1 w-0 grow truncate'>{payload.label[language]}</div> <div className='ml-2 text-sm text-gray-900 flex-1 w-0 grow truncate'>{payload.label[language]}</div>
</div> </div>
{hasAction && (
<FoldIcon className={cn('w-4 h-4 text-text-quaternary shrink-0', isFold && 'text-text-tertiary')} /> <div className='flex items-center'>
)} {isFlatView && (
<div className='text-text-tertiary system-xs-regular'>{groupName}</div>
)}
{hasAction && (
<FoldIcon className={cn('w-4 h-4 text-text-quaternary shrink-0', isFold && 'text-text-tertiary')} />
)}
</div>
</div> </div>
{hasAction && isFold && ( {hasAction && isFold && (
actions.map(action => ( actions.map(action => (
<ActonItem <ActonItem
key={action.name} key={action.name}
className={cn(isShowLetterIndex && 'mr-6')}
provider={payload} provider={payload}
payload={action} payload={action}
onSelect={onSelect} onSelect={onSelect}

View File

@ -28,7 +28,6 @@ const Blocks = ({
const { t } = useTranslation() const { t } = useTranslation()
const language = useGetLanguage() const language = useGetLanguage()
const isFlatView = viewType === ViewType.flat const isFlatView = viewType === ViewType.flat
const isTreeView = viewType === ViewType.tree
const isShowLetterIndex = isFlatView && tools.length > 10 const isShowLetterIndex = isFlatView && tools.length > 10
/* /*
@ -61,6 +60,16 @@ const Blocks = ({
return result return result
}, [withLetterAndGroupViewToolsData]) }, [withLetterAndGroupViewToolsData])
const listViewToolData = useMemo(() => {
const result: ToolWithProvider[] = []
Object.keys(withLetterAndGroupViewToolsData).forEach((letter) => {
Object.keys(withLetterAndGroupViewToolsData[letter]).forEach((groupName) => {
result.push(...withLetterAndGroupViewToolsData[letter][groupName])
})
})
return result
}, [withLetterAndGroupViewToolsData])
const toolRefs = useRef({}) const toolRefs = useRef({})
return ( return (
@ -78,6 +87,9 @@ const Blocks = ({
{!!tools.length && ( {!!tools.length && (
isFlatView ? ( isFlatView ? (
<ToolListFlatView <ToolListFlatView
payload={listViewToolData}
isShowLetterIndex={isShowLetterIndex}
onSelect={onSelect}
/> />
) : ( ) : (
<ToolListTreeView <ToolListTreeView