feat: Add SearchMenu icon and integrate it into the file list component with empty state handling

This commit is contained in:
twwu 2025-06-30 17:31:27 +08:00
parent 1f5c32525f
commit 310102bebd
12 changed files with 233 additions and 51 deletions

View File

@ -0,0 +1,7 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.0049 16.5C28.0049 20.9183 24.4231 24.5 20.0049 24.5C15.5866 24.5 12.0049 20.9183 12.0049 16.5C12.0049 12.0817 15.5866 8.5 20.0049 8.5C24.4231 8.5 28.0049 12.0817 28.0049 16.5Z" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 16.5H6.67155" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 9.83301H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 23.167H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 22.5L29.3333 25.8333" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 844 B

View File

@ -0,0 +1,77 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "32",
"height": "33",
"viewBox": "0 0 32 33",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M28.0049 16.5C28.0049 20.9183 24.4231 24.5 20.0049 24.5C15.5866 24.5 12.0049 20.9183 12.0049 16.5C12.0049 12.0817 15.5866 8.5 20.0049 8.5C24.4231 8.5 28.0049 12.0817 28.0049 16.5Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 16.5H6.67155",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 9.83301H8.00488",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 23.167H8.00488",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M26 22.5L29.3333 25.8333",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "SearchMenu"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './SearchMenu.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'SearchMenu'
export default Icon

View File

@ -9,4 +9,5 @@ export { default as HighQuality } from './HighQuality'
export { default as HybridSearch } from './HybridSearch'
export { default as ParentChildChunk } from './ParentChildChunk'
export { default as QuestionAndAnswer } from './QuestionAndAnswer'
export { default as SearchMenu } from './SearchMenu'
export { default as VectorSearch } from './VectorSearch'

View File

@ -4,14 +4,12 @@ import { useTranslation } from 'react-i18next'
type BreadcrumbsProps = {
prefix: string[]
keywords: string
resetKeywords: () => void
searchResultsLength: number
}
const Breadcrumbs = ({
prefix,
keywords,
resetKeywords,
searchResultsLength,
}: BreadcrumbsProps) => {
const { t } = useTranslation()

View File

@ -1,51 +1,32 @@
import React, { useState } from 'react'
import React from 'react'
import Breadcrumbs from './breadcrumbs'
import Input from '@/app/components/base/input'
import { useDebounceFn } from 'ahooks'
import { useTranslation } from 'react-i18next'
type HeaderProps = {
prefix: string[]
inputValue: string
keywords: string
resetKeywords: () => void
updateKeywords: (keywords: string) => void
searchResultsLength: number
handleInputChange: React.ChangeEventHandler<HTMLInputElement>
handleResetKeywords: () => void
}
const Header = ({
prefix,
inputValue,
keywords,
resetKeywords,
updateKeywords,
searchResultsLength,
handleInputChange,
handleResetKeywords,
}: HeaderProps) => {
const { t } = useTranslation()
const [inputValue, setInputValue] = useState(keywords)
const { run: updateKeywordsWithDebounce } = useDebounceFn(
(keywords: string) => {
updateKeywords(keywords)
},
{ wait: 500 },
)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const keywords = e.target.value
setInputValue(keywords)
updateKeywordsWithDebounce(keywords)
}
const handleResetKeywords = () => {
setInputValue('')
resetKeywords()
}
return (
<div className='flex items-center gap-x-2 bg-components-panel-bg p-1 pl-3'>
<Breadcrumbs
prefix={prefix}
keywords={keywords}
resetKeywords={resetKeywords}
searchResultsLength={searchResultsLength}
/>
<Input

View File

@ -1,6 +1,8 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import Header from './header'
import List from './list'
import { useState } from 'react'
import { useDebounceFn } from 'ahooks'
type FileListProps = {
fileList: OnlineDriveFile[]
@ -21,18 +23,41 @@ const FileList = ({
updateKeywords,
searchResultsLength,
}: FileListProps) => {
const [inputValue, setInputValue] = useState(keywords)
const { run: updateKeywordsWithDebounce } = useDebounceFn(
(keywords: string) => {
updateKeywords(keywords)
},
{ wait: 500 },
)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const keywords = e.target.value
setInputValue(keywords)
updateKeywordsWithDebounce(keywords)
}
const handleResetKeywords = () => {
setInputValue('')
resetKeywords()
}
return (
<div className='flex h-[400px] flex-col overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg shadow-xs shadow-shadow-shadow-3'>
<Header
prefix={prefix}
inputValue={inputValue}
keywords={keywords}
resetKeywords={resetKeywords}
updateKeywords={updateKeywords}
handleInputChange={handleInputChange}
searchResultsLength={searchResultsLength}
handleResetKeywords={handleResetKeywords}
/>
<List
fileList={fileList}
selectedFileList={selectedFileList}
keywords={keywords}
handleResetKeywords={handleResetKeywords}
/>
</div>
)

View File

@ -0,0 +1,14 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
const EmptyFolder = () => {
const { t } = useTranslation()
return (
<div className='flex size-full items-center justify-center rounded-[10px] bg-background-section px-1 py-1.5'>
<span className='system-xs-regular text-text-tertiary'>{t('datasetPipeline.onlineDrive.emptyFolder')}</span>
</div>
)
}
export default React.memo(EmptyFolder)

View File

@ -0,0 +1,35 @@
import Button from '@/app/components/base/button'
import { SearchMenu } from '@/app/components/base/icons/src/vender/knowledge'
import React from 'react'
import { useTranslation } from 'react-i18next'
type EmptySearchResultProps = {
onResetKeywords: () => void
}
const EmptySearchResult = ({
onResetKeywords,
}: EmptySearchResultProps & {
className?: string
}) => {
const { t } = useTranslation()
return (
<div className='flex size-full flex-col items-center justify-center gap-y-2 rounded-[10px] bg-background-section p-6'>
<SearchMenu className='size-8 text-text-tertiary' />
<div className='system-sm-regular text-text-secondary'>
{t('datasetPipeline.onlineDrive.emptySearchResult')}
</div>
<Button
variant='secondary-accent'
size='small'
onClick={onResetKeywords}
className='px-1.5'
>
<span className='px-[3px]'>{t('datasetPipeline.onlineDrive.resetKeywords')}</span>
</Button>
</div>
)
}
export default React.memo(EmptySearchResult)

View File

@ -1,33 +1,55 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import Item from './item'
import EmptyFolder from './empty-folder'
import EmptySearchResult from './empty-search-result'
type FileListProps = {
fileList: OnlineDriveFile[]
selectedFileList: string[]
keywords: string
handleResetKeywords: () => void
}
const List = ({
fileList,
selectedFileList,
keywords,
handleResetKeywords,
}: FileListProps) => {
const isEmptyFolder = fileList.length === 0 && keywords.length === 0
const isSearchResultEmpty = fileList.length === 0 && keywords.length > 0
return (
<div className='grow overflow-y-auto p-1 pt-0'>
<div className='flex flex-col gap-y-px px-1 py-1.5'>
{
fileList.map((file) => {
const isSelected = selectedFileList.includes(file.key)
return (
<Item
key={file.key}
file={file}
isSelected={isSelected}
onSelect={(file) => { console.log(file) }}
onOpen={(file) => { console.log(file) }}
/>
)
})
}
</div>
<div className='grow overflow-hidden p-1 pt-0'>
{
isEmptyFolder && (
<EmptyFolder />
)
}
{
isSearchResultEmpty && (
<EmptySearchResult onResetKeywords={handleResetKeywords} />
)
}
{fileList.length > 0 && (
<div className='flex h-full flex-col gap-y-px overflow-y-auto rounded-[10px] bg-background-section px-1 py-1.5'>
{
fileList.map((file) => {
const isSelected = selectedFileList.includes(file.key)
return (
<Item
key={file.key}
file={file}
isSelected={isSelected}
onSelect={(file) => { console.log(file) }}
onOpen={(file) => { console.log(file) }}
disabled
/>
)
})
}
</div>
)}
</div>
)
}

View File

@ -115,11 +115,12 @@ const translation = {
breadcrumbs: {
allBuckets: 'All Cloud Storage Buckets',
searchResult: 'Find {{searchResultsLength}} items in "{{folderName}}" folder',
noSearchResult: 'No items were found',
resetKeywords: 'Reset keywords',
searchPlaceholder: 'Search files...',
},
notSupportedFileType: 'This file type is not supported',
emptyFolder: 'This folder is empty',
emptySearchResult: 'No items were found',
resetKeywords: 'Reset keywords',
},
}

View File

@ -115,11 +115,12 @@ const translation = {
breadcrumbs: {
allBuckets: '所有云存储桶',
searchResult: '在 "{{folderName}}" 文件夹中找到 {{searchResultsLength}} 个项目',
noSearchResult: '未找到项目',
resetKeywords: '重置关键词',
searchPlaceholder: '搜索文件...',
},
notSupportedFileType: '不支持此文件类型',
emptyFolder: '此文件夹为空',
emptySearchResult: '未找到任何项目',
resetKeywords: '重置关键词',
},
}