refactor: Restructure breadcrumbs component; introduce Bucket and BreadcrumbItem components for improved navigation

This commit is contained in:
twwu 2025-07-04 16:44:21 +08:00
parent 9ce0c69687
commit d44af3ec46
10 changed files with 197 additions and 65 deletions

View File

@ -1,57 +0,0 @@
import { BucketsGray } from '@/app/components/base/icons/src/public/knowledge/online-drive'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useDataSourceStore } from '../../../store'
import Tooltip from '@/app/components/base/tooltip'
type BreadcrumbsProps = {
prefix: string[]
keywords: string
bucket: string
searchResultsLength: number
}
const Breadcrumbs = ({
prefix,
keywords,
bucket,
searchResultsLength,
}: BreadcrumbsProps) => {
const { t } = useTranslation()
const { setFileList, setSelectedFileList, setPrefix, setBucket } = useDataSourceStore().getState()
const isRoot = prefix.length === 0 && bucket === ''
const isSearching = !!keywords
const handleBackToBucketList = useCallback(() => {
setFileList([])
setSelectedFileList([])
setBucket('')
setPrefix([])
}, [setBucket, setFileList, setPrefix, setSelectedFileList])
return (
<div className='flex grow items-center py-1'>
{isRoot && (
<div className='system-sm-medium text-test-secondary px-[5px] py-1'>
{t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
</div>
)}
{!isRoot && (
<div className='flex items-center gap-x-0.5'>
<Tooltip
popupContent={t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
>
<div
className='flex size-5 cursor-pointer items-center justify-center'
onClick={handleBackToBucketList}
>
<BucketsGray />
</div>
</Tooltip>
</div>
)}
</div>
)
}
export default React.memo(Breadcrumbs)

View File

@ -0,0 +1,33 @@
import React from 'react'
import { BucketsGray } from '@/app/components/base/icons/src/public/knowledge/online-drive'
import Tooltip from '@/app/components/base/tooltip'
import { useTranslation } from 'react-i18next'
type BucketProps = {
handleBackToBucketList: () => void
}
const Bucket = ({
handleBackToBucketList,
}: BucketProps) => {
const { t } = useTranslation()
return (
<>
<Tooltip
popupContent={t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
>
<button
type='button'
className='flex size-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={handleBackToBucketList}
>
<BucketsGray />
</button>
</Tooltip>
<span className='system-xs-regular text-divider-deep'>/</span>
</>
)
}
export default React.memo(Bucket)

View File

@ -0,0 +1,98 @@
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useDataSourceStore } from '../../../../store'
import Bucket from './bucket'
import BreadcrumbItem from './item'
type BreadcrumbsProps = {
prefix: string[]
keywords: string
bucket: string
searchResultsLength: number
isInPipeline: boolean
}
const Breadcrumbs = ({
prefix,
keywords,
bucket,
searchResultsLength,
isInPipeline,
}: BreadcrumbsProps) => {
const { t } = useTranslation()
const { setFileList, setSelectedFileList, setPrefix, setBucket } = useDataSourceStore().getState()
const showSearchResult = !!keywords && searchResultsLength > 0
const isRoot = prefix.length === 0 && bucket === ''
const breadcrumbs = useMemo(() => {
const displayBreadcrumbNum = isInPipeline ? 2 : 3
const prefixToDisplay = prefix.slice(0, displayBreadcrumbNum - 1)
const collapsedBreadcrumbs = prefix.slice(displayBreadcrumbNum - 1, prefix.length - 1)
return {
original: prefix,
needCollapsed: prefix.length > displayBreadcrumbNum,
prefixBreadcrumbs: prefixToDisplay,
collapsedBreadcrumbs,
lastBreadcrumb: prefix[prefix.length - 1],
}
}, [isInPipeline, prefix])
const handleBackToBucketList = useCallback(() => {
setFileList([])
setSelectedFileList([])
setBucket('')
setPrefix([])
}, [setBucket, setFileList, setPrefix, setSelectedFileList])
const handleClickBreadcrumb = useCallback((index: number) => {
const newPrefix = breadcrumbs.prefixBreadcrumbs.slice(0, index - 1)
setFileList([])
setSelectedFileList([])
setPrefix(newPrefix)
}, [breadcrumbs.prefixBreadcrumbs, setFileList, setPrefix, setSelectedFileList])
return (
<div className='flex grow items-center py-1'>
{showSearchResult && (
<div className='system-sm-medium text-test-secondary px-[5px] py-1'>
{t('datasetPipeline.onlineDrive.breadcrumbs.searchResult', {
searchResultsLength,
folderName: prefix.length > 0 ? prefix[prefix.length - 1] : bucket,
})}
</div>
)}
{!showSearchResult && isRoot && (
<div className='system-sm-medium text-test-secondary px-[5px] py-1'>
{t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
</div>
)}
{!showSearchResult && !isRoot && (
<div className='flex items-center gap-x-0.5'>
{bucket && (
<Bucket handleBackToBucketList={handleBackToBucketList} />
)}
{!breadcrumbs.needCollapsed && (
<>
{breadcrumbs.original.map((breadcrumb, index) => {
const isLast = index === breadcrumbs.original.length - 1
return (
<BreadcrumbItem
key={`${breadcrumb}-${index}`}
index={index}
handleClick={handleClickBreadcrumb}
name={breadcrumb}
isActive={isLast}
showSeparator={!isLast}
disabled={isLast}
/>
)
})}
</>
)}
</div>
)}
</div>
)
}
export default React.memo(Breadcrumbs)

View File

@ -0,0 +1,47 @@
import React, { useCallback } from 'react'
import cn from '@/utils/classnames'
type BreadcrumbItemProps = {
name: string
index: number
handleClick: (index: number) => void
disabled?: boolean
isActive?: boolean
showSeparator?: boolean
}
const BreadcrumbItem = ({
name,
index,
handleClick,
disabled = false,
isActive = false,
showSeparator = true,
}: BreadcrumbItemProps) => {
const handleClickItem = useCallback(() => {
if (!disabled)
handleClick(index)
}, [disabled, handleClick, index])
return (
<>
<button
type='button'
className={cn(
'rounded-md px-[5px] py-1',
isActive ? 'system-sm-medium text-text-secondary' : 'system-sm-regular text-text-tertiary',
!disabled && 'hover:bg-state-base-hover',
)}
disabled={disabled}
onClick={handleClickItem}
>
{name}
</button>
{showSeparator && <span className='system-xs-regular text-divider-deep'>/</span>}
</>
)
}
BreadcrumbItem.displayName = 'BreadcrumbItem'
export default React.memo(BreadcrumbItem)

View File

@ -11,6 +11,7 @@ type HeaderProps = {
searchResultsLength: number
handleInputChange: React.ChangeEventHandler<HTMLInputElement>
handleResetKeywords: () => void
isInPipeline: boolean
}
const Header = ({
@ -18,6 +19,7 @@ const Header = ({
inputValue,
keywords,
bucket,
isInPipeline,
searchResultsLength,
handleInputChange,
handleResetKeywords,
@ -31,6 +33,7 @@ const Header = ({
keywords={keywords}
bucket={bucket}
searchResultsLength={searchResultsLength}
isInPipeline={isInPipeline}
/>
<Input
value={inputValue}

View File

@ -60,6 +60,7 @@ const FileList = ({
inputValue={inputValue}
keywords={keywords}
bucket={bucket}
isInPipeline={isInPipeline}
handleInputChange={handleInputChange}
searchResultsLength={searchResultsLength}
handleResetKeywords={handleResetKeywords}

View File

@ -66,12 +66,12 @@ const Item = ({
'flex grow items-center gap-x-1 overflow-hidden py-0.5',
disabled && 'opacity-30',
)}>
<FileIcon type={file.type} fileName={file.key} className='shrink-0' />
<FileIcon type={file.type} fileName={file.displayName} className='shrink-0' />
<span
className='system-sm-medium grow truncate text-text-secondary'
title={file.key}
title={file.displayName}
>
{file.key}
{file.displayName}
</span>
{!isFolder && file.size && (
<span className='system-xs-regular shrink-0 text-text-tertiary'>{formatFileSize(file.size)}</span>

View File

@ -43,13 +43,14 @@ const OnlineDrive = ({
: `/rag/pipelines/${pipelineId}/workflows/draft/datasource/nodes/${nodeId}/run`
const getOnlineDrive = useCallback(async () => {
const prefixString = prefix.length > 0 ? `${prefix.join('/')}/` : ''
setIsLoading(true)
ssePost(
datasourceNodeRunURL,
{
body: {
inputs: {
prefix: prefix.join('/'),
prefix: prefixString,
bucket,
start_after: startAfter,
max_keys: 30, // Adjust as needed
@ -111,11 +112,12 @@ const OnlineDrive = ({
if (file.type === OnlineDriveFileType.file) return
setFileList([])
if (file.type === OnlineDriveFileType.bucket) {
setBucket(file.key)
setBucket(file.displayName)
}
else {
const key = file.displayName.endsWith('/') ? file.displayName.slice(0, -1) : file.displayName
const newPrefix = produce(prefix, (draft) => {
const newList = file.key.split('/')
const newList = key.split('/')
draft.push(...newList)
})
setPrefix(newPrefix)

View File

@ -18,16 +18,20 @@ export const convertOnlineDriveDataToFileList = (data: OnlineDriveData[]): Onlin
data.forEach((item) => {
fileList.push({
key: item.bucket,
displayName: item.bucket,
type: OnlineDriveFileType.bucket,
})
})
}
else {
data[0].files.forEach((file) => {
const isFileType = isFile(file.key)
const filePathList = file.key.split('/')
fileList.push({
key: file.key,
size: isFile(file.key) ? file.size : undefined,
type: isFile(file.key) ? OnlineDriveFileType.file : OnlineDriveFileType.folder,
displayName: `${isFileType ? filePathList.pop() : filePathList[filePathList.length - 2]}${isFileType ? '' : '/'}`,
size: isFileType ? file.size : undefined,
type: isFileType ? OnlineDriveFileType.file : OnlineDriveFileType.folder,
})
})
}

View File

@ -276,6 +276,7 @@ export enum OnlineDriveFileType {
export type OnlineDriveFile = {
key: string
displayName: string
size?: number
type: OnlineDriveFileType
}