mirror of
https://github.com/langgenius/dify.git
synced 2025-11-12 01:11:53 +00:00
feat: Add SearchMenu icon and integrate it into the file list component with empty state handling
This commit is contained in:
parent
1f5c32525f
commit
310102bebd
@ -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 |
@ -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"
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -9,4 +9,5 @@ export { default as HighQuality } from './HighQuality'
|
|||||||
export { default as HybridSearch } from './HybridSearch'
|
export { default as HybridSearch } from './HybridSearch'
|
||||||
export { default as ParentChildChunk } from './ParentChildChunk'
|
export { default as ParentChildChunk } from './ParentChildChunk'
|
||||||
export { default as QuestionAndAnswer } from './QuestionAndAnswer'
|
export { default as QuestionAndAnswer } from './QuestionAndAnswer'
|
||||||
|
export { default as SearchMenu } from './SearchMenu'
|
||||||
export { default as VectorSearch } from './VectorSearch'
|
export { default as VectorSearch } from './VectorSearch'
|
||||||
|
|||||||
@ -4,14 +4,12 @@ import { useTranslation } from 'react-i18next'
|
|||||||
type BreadcrumbsProps = {
|
type BreadcrumbsProps = {
|
||||||
prefix: string[]
|
prefix: string[]
|
||||||
keywords: string
|
keywords: string
|
||||||
resetKeywords: () => void
|
|
||||||
searchResultsLength: number
|
searchResultsLength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const Breadcrumbs = ({
|
const Breadcrumbs = ({
|
||||||
prefix,
|
prefix,
|
||||||
keywords,
|
keywords,
|
||||||
resetKeywords,
|
|
||||||
searchResultsLength,
|
searchResultsLength,
|
||||||
}: BreadcrumbsProps) => {
|
}: BreadcrumbsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@ -1,51 +1,32 @@
|
|||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import Breadcrumbs from './breadcrumbs'
|
import Breadcrumbs from './breadcrumbs'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import { useDebounceFn } from 'ahooks'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
type HeaderProps = {
|
type HeaderProps = {
|
||||||
prefix: string[]
|
prefix: string[]
|
||||||
|
inputValue: string
|
||||||
keywords: string
|
keywords: string
|
||||||
resetKeywords: () => void
|
|
||||||
updateKeywords: (keywords: string) => void
|
|
||||||
searchResultsLength: number
|
searchResultsLength: number
|
||||||
|
handleInputChange: React.ChangeEventHandler<HTMLInputElement>
|
||||||
|
handleResetKeywords: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = ({
|
const Header = ({
|
||||||
prefix,
|
prefix,
|
||||||
|
inputValue,
|
||||||
keywords,
|
keywords,
|
||||||
resetKeywords,
|
|
||||||
updateKeywords,
|
|
||||||
searchResultsLength,
|
searchResultsLength,
|
||||||
|
handleInputChange,
|
||||||
|
handleResetKeywords,
|
||||||
}: HeaderProps) => {
|
}: HeaderProps) => {
|
||||||
const { t } = useTranslation()
|
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 (
|
return (
|
||||||
<div className='flex items-center gap-x-2 bg-components-panel-bg p-1 pl-3'>
|
<div className='flex items-center gap-x-2 bg-components-panel-bg p-1 pl-3'>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
keywords={keywords}
|
keywords={keywords}
|
||||||
resetKeywords={resetKeywords}
|
|
||||||
searchResultsLength={searchResultsLength}
|
searchResultsLength={searchResultsLength}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||||
import Header from './header'
|
import Header from './header'
|
||||||
import List from './list'
|
import List from './list'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useDebounceFn } from 'ahooks'
|
||||||
|
|
||||||
type FileListProps = {
|
type FileListProps = {
|
||||||
fileList: OnlineDriveFile[]
|
fileList: OnlineDriveFile[]
|
||||||
@ -21,18 +23,41 @@ const FileList = ({
|
|||||||
updateKeywords,
|
updateKeywords,
|
||||||
searchResultsLength,
|
searchResultsLength,
|
||||||
}: FileListProps) => {
|
}: 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 (
|
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'>
|
<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
|
<Header
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
|
inputValue={inputValue}
|
||||||
keywords={keywords}
|
keywords={keywords}
|
||||||
resetKeywords={resetKeywords}
|
handleInputChange={handleInputChange}
|
||||||
updateKeywords={updateKeywords}
|
|
||||||
searchResultsLength={searchResultsLength}
|
searchResultsLength={searchResultsLength}
|
||||||
|
handleResetKeywords={handleResetKeywords}
|
||||||
/>
|
/>
|
||||||
<List
|
<List
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
selectedFileList={selectedFileList}
|
selectedFileList={selectedFileList}
|
||||||
|
keywords={keywords}
|
||||||
|
handleResetKeywords={handleResetKeywords}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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)
|
||||||
@ -1,33 +1,55 @@
|
|||||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
|
import EmptyFolder from './empty-folder'
|
||||||
|
import EmptySearchResult from './empty-search-result'
|
||||||
|
|
||||||
type FileListProps = {
|
type FileListProps = {
|
||||||
fileList: OnlineDriveFile[]
|
fileList: OnlineDriveFile[]
|
||||||
selectedFileList: string[]
|
selectedFileList: string[]
|
||||||
|
keywords: string
|
||||||
|
handleResetKeywords: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const List = ({
|
const List = ({
|
||||||
fileList,
|
fileList,
|
||||||
selectedFileList,
|
selectedFileList,
|
||||||
|
keywords,
|
||||||
|
handleResetKeywords,
|
||||||
}: FileListProps) => {
|
}: FileListProps) => {
|
||||||
|
const isEmptyFolder = fileList.length === 0 && keywords.length === 0
|
||||||
|
const isSearchResultEmpty = fileList.length === 0 && keywords.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grow overflow-y-auto p-1 pt-0'>
|
<div className='grow overflow-hidden p-1 pt-0'>
|
||||||
<div className='flex flex-col gap-y-px px-1 py-1.5'>
|
{
|
||||||
{
|
isEmptyFolder && (
|
||||||
fileList.map((file) => {
|
<EmptyFolder />
|
||||||
const isSelected = selectedFileList.includes(file.key)
|
)
|
||||||
return (
|
}
|
||||||
<Item
|
{
|
||||||
key={file.key}
|
isSearchResultEmpty && (
|
||||||
file={file}
|
<EmptySearchResult onResetKeywords={handleResetKeywords} />
|
||||||
isSelected={isSelected}
|
)
|
||||||
onSelect={(file) => { console.log(file) }}
|
}
|
||||||
onOpen={(file) => { console.log(file) }}
|
{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)
|
||||||
</div>
|
return (
|
||||||
|
<Item
|
||||||
|
key={file.key}
|
||||||
|
file={file}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelect={(file) => { console.log(file) }}
|
||||||
|
onOpen={(file) => { console.log(file) }}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,11 +115,12 @@ const translation = {
|
|||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
allBuckets: 'All Cloud Storage Buckets',
|
allBuckets: 'All Cloud Storage Buckets',
|
||||||
searchResult: 'Find {{searchResultsLength}} items in "{{folderName}}" folder',
|
searchResult: 'Find {{searchResultsLength}} items in "{{folderName}}" folder',
|
||||||
noSearchResult: 'No items were found',
|
|
||||||
resetKeywords: 'Reset keywords',
|
|
||||||
searchPlaceholder: 'Search files...',
|
searchPlaceholder: 'Search files...',
|
||||||
},
|
},
|
||||||
notSupportedFileType: 'This file type is not supported',
|
notSupportedFileType: 'This file type is not supported',
|
||||||
|
emptyFolder: 'This folder is empty',
|
||||||
|
emptySearchResult: 'No items were found',
|
||||||
|
resetKeywords: 'Reset keywords',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,11 +115,12 @@ const translation = {
|
|||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
allBuckets: '所有云存储桶',
|
allBuckets: '所有云存储桶',
|
||||||
searchResult: '在 "{{folderName}}" 文件夹中找到 {{searchResultsLength}} 个项目',
|
searchResult: '在 "{{folderName}}" 文件夹中找到 {{searchResultsLength}} 个项目',
|
||||||
noSearchResult: '未找到项目',
|
|
||||||
resetKeywords: '重置关键词',
|
|
||||||
searchPlaceholder: '搜索文件...',
|
searchPlaceholder: '搜索文件...',
|
||||||
},
|
},
|
||||||
notSupportedFileType: '不支持此文件类型',
|
notSupportedFileType: '不支持此文件类型',
|
||||||
|
emptyFolder: '此文件夹为空',
|
||||||
|
emptySearchResult: '未找到任何项目',
|
||||||
|
resetKeywords: '重置关键词',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user