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

View File

@ -1,5 +1,5 @@
'use client' 'use client'
import { useRef } from 'react' import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { BlockEnum } from '../types' import type { BlockEnum } from '../types'
import type { ToolDefaultValue } from './types' import type { ToolDefaultValue } from './types'
@ -10,12 +10,15 @@ import cn from '@/utils/classnames'
import Link from 'next/link' import Link from 'next/link'
import { RiArrowRightUpLine } from '@remixicon/react' import { RiArrowRightUpLine } from '@remixicon/react'
import { getMarketplaceUrl } from '@/utils/var' 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 = { type AllStartBlocksProps = {
className?: string className?: string
searchText: string searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[] availableBlocksTypes?: BlockEnum[]
tags?: string[]
} }
const AllStartBlocks = ({ const AllStartBlocks = ({
@ -23,26 +26,68 @@ const AllStartBlocks = ({
searchText, searchText,
onSelect, onSelect,
availableBlocksTypes, availableBlocksTypes,
tags = [],
}: AllStartBlocksProps) => { }: AllStartBlocksProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const wrapElemRef = useRef<HTMLDivElement>(null) 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 ( return (
<div className={cn('min-w-[400px] max-w-[500px]', className)}> <div className={cn('min-w-[400px] max-w-[500px]', className)}>
<div <div
ref={wrapElemRef} ref={wrapElemRef}
className='max-h-[464px] overflow-y-auto' className='h-[640px] max-h-[640px] overflow-y-auto'
> >
<StartBlocks {shouldShowEmptyState && (
searchText={searchText} <div className='flex flex-col items-center gap-1 pt-48'>
onSelect={onSelect} <SearchMenu className='h-8 w-8 text-text-quaternary' />
availableBlocksTypes={ENTRY_NODE_TYPES as unknown as BlockEnum[]} <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 {!shouldShowEmptyState && (
onSelect={onSelect} <>
searchText={searchText} <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> </div>
{/* Footer - Same as Tools tab marketplace footer */} {/* Footer - Same as Tools tab marketplace footer */}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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