mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 20:48:01 +08:00
Improve workflow block selector search functionality (#24707)
This commit is contained in:
parent
89ad6ad902
commit
64c7be59b7
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,7 @@ const Tabs: FC<TabsProps> = ({
|
|||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
availableBlocksTypes={availableBlocksTypes}
|
availableBlocksTypes={availableBlocksTypes}
|
||||||
|
tags={tags}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -244,6 +244,8 @@ const translation = {
|
|||||||
'transform': '変換',
|
'transform': '変換',
|
||||||
'utilities': 'ツール',
|
'utilities': 'ツール',
|
||||||
'noResult': '該当なし',
|
'noResult': '該当なし',
|
||||||
|
'noPluginsFound': 'プラグインが見つかりません',
|
||||||
|
'requestToCommunity': 'コミュニティにリクエスト',
|
||||||
'plugin': 'プラグイン',
|
'plugin': 'プラグイン',
|
||||||
'agent': 'エージェント戦略',
|
'agent': 'エージェント戦略',
|
||||||
'addAll': 'すべてを追加する',
|
'addAll': 'すべてを追加する',
|
||||||
|
|||||||
@ -245,6 +245,8 @@ const translation = {
|
|||||||
'transform': '转换',
|
'transform': '转换',
|
||||||
'utilities': '工具',
|
'utilities': '工具',
|
||||||
'noResult': '未找到匹配项',
|
'noResult': '未找到匹配项',
|
||||||
|
'noPluginsFound': '未找到插件',
|
||||||
|
'requestToCommunity': '向社区反馈',
|
||||||
'agent': 'Agent 策略',
|
'agent': 'Agent 策略',
|
||||||
'allAdded': '已添加全部',
|
'allAdded': '已添加全部',
|
||||||
'addAll': '添加全部',
|
'addAll': '添加全部',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user