Improve workflow block selector search functionality (#24707)

This commit is contained in:
lyzno1 2025-08-28 17:21:34 +08:00 committed by GitHub
parent 89ad6ad902
commit 64c7be59b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 130 additions and 46 deletions

View File

@ -1,5 +1,5 @@
'use client'
import { RiCloseLine, RiSearchLine } from '@remixicon/react'
import { RiCloseCircleFill, RiSearchLine } from '@remixicon/react'
import TagsFilter from './tags-filter'
import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
@ -48,7 +48,7 @@ const SearchBox = ({
<RiSearchLine className='mr-1.5 size-4 text-text-placeholder' />
<input
className={cn(
'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none',
'system-sm-regular block grow appearance-none bg-transparent text-text-secondary outline-none',
)}
value={search}
onChange={(e) => {
@ -58,10 +58,8 @@ const SearchBox = ({
/>
{
search && (
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
<ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
<div className={cn('group absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer p-[1px]')} onClick={() => onSearchChange('')}>
<RiCloseCircleFill className='h-3.5 w-3.5 cursor-pointer text-text-quaternary group-hover:text-text-tertiary' />
</div>
)
}

View File

@ -1,5 +1,5 @@
'use client'
import { useRef } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { BlockEnum } from '../types'
import type { ToolDefaultValue } from './types'
@ -10,12 +10,15 @@ import cn from '@/utils/classnames'
import Link from 'next/link'
import { RiArrowRightUpLine } from '@remixicon/react'
import { getMarketplaceUrl } from '@/utils/var'
import Button from '@/app/components/base/button'
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
type AllStartBlocksProps = {
className?: string
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
tags?: string[]
}
const AllStartBlocks = ({
@ -23,26 +26,68 @@ const AllStartBlocks = ({
searchText,
onSelect,
availableBlocksTypes,
tags = [],
}: AllStartBlocksProps) => {
const { t } = useTranslation()
const wrapElemRef = useRef<HTMLDivElement>(null)
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
const [hasPluginContent, setHasPluginContent] = useState(false)
const handleStartBlocksContentChange = useCallback((hasContent: boolean) => {
setHasStartBlocksContent(hasContent)
}, [])
const handlePluginContentChange = useCallback((hasContent: boolean) => {
setHasPluginContent(hasContent)
}, [])
const hasAnyContent = hasStartBlocksContent || hasPluginContent
const shouldShowEmptyState = searchText && !hasAnyContent
return (
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
<div
ref={wrapElemRef}
className='max-h-[464px] overflow-y-auto'
className='h-[640px] max-h-[640px] overflow-y-auto'
>
<StartBlocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={ENTRY_NODE_TYPES as unknown as BlockEnum[]}
/>
{shouldShowEmptyState && (
<div className='flex flex-col items-center gap-1 pt-48'>
<SearchMenu className='h-8 w-8 text-text-quaternary' />
<div className='text-sm font-medium text-text-secondary'>
{t('workflow.tabs.noPluginsFound')}
</div>
<Link
href='https://github.com/langgenius/dify-plugins/issues'
target='_blank'
>
<Button
size='small'
variant='secondary-accent'
className='h-6 px-3 text-xs'
>
{t('workflow.tabs.requestToCommunity')}
</Button>
</Link>
</div>
)}
<TriggerPluginSelector
onSelect={onSelect}
searchText={searchText}
/>
{!shouldShowEmptyState && (
<>
<StartBlocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={ENTRY_NODE_TYPES as unknown as BlockEnum[]}
onContentStateChange={handleStartBlocksContentChange}
/>
<TriggerPluginSelector
onSelect={onSelect}
searchText={searchText}
onContentStateChange={handlePluginContentChange}
tags={tags}
/>
</>
)}
</div>
{/* Footer - Same as Tools tab marketplace footer */}

View File

@ -143,7 +143,18 @@ const NodeSelector: FC<NodeSelectorProps> = ({
onActiveTabChange={handleActiveTabChange}
filterElem={
<div className='relative m-2' onClick={e => e.stopPropagation()}>
{(activeTab === TabsEnum.Start || activeTab === TabsEnum.Blocks) && (
{activeTab === TabsEnum.Start && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={searchPlaceholder}
inputClassName='grow'
/>
)}
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
@ -161,7 +172,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
placeholder={searchPlaceholder}
inputClassName='grow'
/>
)}

View File

@ -1,6 +1,7 @@
import {
memo,
useCallback,
useEffect,
useMemo,
} from 'react'
import { useNodes } from 'reactflow'
@ -17,12 +18,14 @@ type StartBlocksProps = {
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
onContentStateChange?: (hasContent: boolean) => void
}
const StartBlocks = ({
searchText,
onSelect,
availableBlocksTypes = [],
onContentStateChange,
}: StartBlocksProps) => {
const { t } = useTranslation()
const nodes = useNodes()
@ -48,6 +51,10 @@ const StartBlocks = ({
const isEmpty = filteredBlocks.length === 0
useEffect(() => {
onContentStateChange?.(!isEmpty)
}, [isEmpty, onContentStateChange])
const renderBlock = useCallback((block: typeof START_BLOCKS[0]) => (
<Tooltip
key={block.type}
@ -84,27 +91,23 @@ const StartBlocks = ({
</Tooltip>
), [nodesExtraData, onSelect, t])
if (isEmpty)
return null
return (
<div className='min-w-[400px] max-w-[500px] p-1'>
{isEmpty && (
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>
{t('workflow.tabs.noResult')}
</div>
)}
{!isEmpty && (
<div className='mb-1'>
{filteredBlocks.map((block, index) => (
<div key={block.type}>
{renderBlock(block)}
{block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && (
<div className='my-1 px-3'>
<div className='border-t border-divider-subtle' />
</div>
)}
</div>
))}
</div>
)}
<div className='p-1'>
<div className='mb-1'>
{filteredBlocks.map((block, index) => (
<div key={block.type}>
{renderBlock(block)}
{block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && (
<div className='my-1 px-3'>
<div className='border-t border-divider-subtle' />
</div>
)}
</div>
))}
</div>
</div>
)
}

View File

@ -72,6 +72,7 @@ const Tabs: FC<TabsProps> = ({
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
tags={tags}
/>
</div>
)

View File

@ -7,16 +7,22 @@ import type { ToolDefaultValue } from './types'
type TriggerPluginSelectorProps = {
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
searchText: string
onContentStateChange?: (hasContent: boolean) => void
tags?: string[]
}
const TriggerPluginSelector = ({
onSelect,
searchText,
onContentStateChange,
tags = [],
}: TriggerPluginSelectorProps) => {
return (
<TriggerPluginList
onSelect={onSelect}
searchText={searchText}
onContentStateChange={onContentStateChange}
tags={tags}
/>
)
}

View File

@ -1,5 +1,5 @@
'use client'
import { memo, useMemo } from 'react'
import { memo, useEffect, useMemo } from 'react'
import { useAllBuiltInTools } from '@/service/use-tools'
import TriggerPluginItem from './item'
import type { BlockEnum } from '../../types'
@ -9,11 +9,15 @@ import { useGetLanguage } from '@/context/i18n'
type TriggerPluginListProps = {
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
searchText: string
onContentStateChange?: (hasContent: boolean) => void
tags?: string[]
}
const TriggerPluginList = ({
onSelect,
searchText,
onContentStateChange,
tags = [],
}: TriggerPluginListProps) => {
const { data: buildInTools = [] } = useAllBuiltInTools()
const language = useGetLanguage()
@ -22,16 +26,26 @@ const TriggerPluginList = ({
return buildInTools.filter((toolWithProvider) => {
if (toolWithProvider.tools.length === 0) return false
if (!searchText) return true
// Filter by search text
if (searchText) {
const matchesSearch = toolWithProvider.name.toLowerCase().includes(searchText.toLowerCase())
|| toolWithProvider.tools.some(tool =>
tool.label[language].toLowerCase().includes(searchText.toLowerCase()),
)
if (!matchesSearch) return false
}
return toolWithProvider.name.toLowerCase().includes(searchText.toLowerCase())
|| toolWithProvider.tools.some(tool =>
tool.label[language].toLowerCase().includes(searchText.toLowerCase()),
)
return true
})
}, [buildInTools, searchText, language])
if (!triggerPlugins.length)
const hasContent = triggerPlugins.length > 0
useEffect(() => {
onContentStateChange?.(hasContent)
}, [hasContent, onContentStateChange])
if (!hasContent)
return null
return (

View File

@ -246,6 +246,8 @@ const translation = {
'transform': 'Transform',
'utilities': 'Utilities',
'noResult': 'No match found',
'noPluginsFound': 'No plugins were found',
'requestToCommunity': 'Requests to the community',
'agent': 'Agent Strategy',
'allAdded': 'All added',
'addAll': 'Add all',

View File

@ -244,6 +244,8 @@ const translation = {
'transform': '変換',
'utilities': 'ツール',
'noResult': '該当なし',
'noPluginsFound': 'プラグインが見つかりません',
'requestToCommunity': 'コミュニティにリクエスト',
'plugin': 'プラグイン',
'agent': 'エージェント戦略',
'addAll': 'すべてを追加する',

View File

@ -245,6 +245,8 @@ const translation = {
'transform': '转换',
'utilities': '工具',
'noResult': '未找到匹配项',
'noPluginsFound': '未找到插件',
'requestToCommunity': '向社区反馈',
'agent': 'Agent 策略',
'allAdded': '已添加全部',
'addAll': '添加全部',