dify
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { useToolIcon } from '@/app/components/workflow/hooks'
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type ConnectProps = {
|
||||
nodeData: DataSourceNodeType
|
||||
onSetting: () => void
|
||||
}
|
||||
|
||||
const Connect = ({
|
||||
nodeData,
|
||||
onSetting,
|
||||
}: ConnectProps) => {
|
||||
const { t } = useTranslation()
|
||||
const toolIcon = useToolIcon(nodeData)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-start gap-y-2 rounded-xl bg-workflow-process-bg p-6'>
|
||||
<div className='flex size-12 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg p-1 shadow-lg shadow-shadow-shadow-5'>
|
||||
<BlockIcon
|
||||
type={BlockEnum.DataSource}
|
||||
toolIcon={toolIcon}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-y-1'>
|
||||
<div className='flex flex-col gap-y-1 pb-3 pt-1'>
|
||||
<div className='system-md-semibold text-text-secondary'>
|
||||
<span className='relative'>
|
||||
{t('datasetPipeline.onlineDrive.notConnected', { name: nodeData.title })}
|
||||
<Icon3Dots className='absolute -right-2.5 -top-1.5 size-4 text-text-secondary' />
|
||||
</span>
|
||||
</div>
|
||||
<div className='system-sm-regular text-text-tertiary'>
|
||||
{t('datasetPipeline.onlineDrive.notConnectedTip', { name: nodeData.title })}
|
||||
</div>
|
||||
</div>
|
||||
<Button className='w-fit' variant='primary' onClick={onSetting}>
|
||||
{t('datasetCreation.stepOne.connect')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Connect
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useCallback } 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'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type BucketProps = {
|
||||
bucketName: string
|
||||
isActive?: boolean
|
||||
disabled?: boolean
|
||||
showSeparator?: boolean
|
||||
handleBackToBucketList: () => void
|
||||
handleClickBucketName: () => void
|
||||
}
|
||||
|
||||
const Bucket = ({
|
||||
bucketName,
|
||||
handleBackToBucketList,
|
||||
handleClickBucketName,
|
||||
disabled = false,
|
||||
isActive = false,
|
||||
showSeparator = true,
|
||||
}: BucketProps) => {
|
||||
const { t } = useTranslation()
|
||||
const handleClickItem = useCallback(() => {
|
||||
if (!disabled)
|
||||
handleClickBucketName()
|
||||
}, [disabled, handleClickBucketName])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='flex size-6 shrink-0 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>
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
'max-w-full shrink truncate 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}
|
||||
title={bucketName}
|
||||
>
|
||||
{bucketName}
|
||||
</button>
|
||||
{showSeparator && <span className='system-xs-regular shrink-0 text-divider-deep'>/</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Bucket)
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type DriveProps = {
|
||||
breadcrumbs: string[]
|
||||
handleBackToRoot: () => void
|
||||
}
|
||||
|
||||
const Drive = ({
|
||||
breadcrumbs,
|
||||
handleBackToRoot,
|
||||
}: DriveProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
'max-w-full shrink truncate rounded-md px-[5px] py-1',
|
||||
breadcrumbs.length > 0 && 'system-sm-regular text-text-tertiary hover:bg-state-base-hover',
|
||||
breadcrumbs.length === 0 && 'system-sm-medium text-text-secondary',
|
||||
)}
|
||||
onClick={handleBackToRoot}
|
||||
disabled={breadcrumbs.length === 0}
|
||||
>
|
||||
{t('datasetPipeline.onlineDrive.breadcrumbs.allFiles')}
|
||||
</button>
|
||||
{breadcrumbs.length > 0 && <span className='system-xs-regular text-divider-deep'>/</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Drive)
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Menu from './menu'
|
||||
|
||||
type DropdownProps = {
|
||||
startIndex: number
|
||||
breadcrumbs: string[]
|
||||
onBreadcrumbClick: (index: number) => void
|
||||
}
|
||||
|
||||
const Dropdown = ({
|
||||
startIndex,
|
||||
breadcrumbs,
|
||||
onBreadcrumbClick,
|
||||
}: DropdownProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
setOpen(prev => !prev)
|
||||
}, [])
|
||||
|
||||
const handleBreadCrumbClick = useCallback((index: number) => {
|
||||
onBreadcrumbClick(index)
|
||||
setOpen(false)
|
||||
}, [onBreadcrumbClick])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -13,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
'flex size-6 items-center justify-center rounded-md',
|
||||
open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<RiMoreFill className='size-4 text-text-tertiary' />
|
||||
</button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<Menu
|
||||
breadcrumbs={breadcrumbs}
|
||||
startIndex={startIndex}
|
||||
onBreadcrumbClick={handleBreadCrumbClick}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
<span className='system-xs-regular text-divider-deep'>/</span>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Dropdown)
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
type ItemProps = {
|
||||
name: string
|
||||
index: number
|
||||
onBreadcrumbClick: (index: number) => void
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
name,
|
||||
index,
|
||||
onBreadcrumbClick,
|
||||
}: ItemProps) => {
|
||||
const handleClick = useCallback(() => {
|
||||
onBreadcrumbClick(index)
|
||||
}, [index, onBreadcrumbClick])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='system-md-regular rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={handleClick}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Item)
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import Item from './item'
|
||||
|
||||
type MenuProps = {
|
||||
breadcrumbs: string[]
|
||||
startIndex: number
|
||||
onBreadcrumbClick: (index: number) => void
|
||||
}
|
||||
|
||||
const Menu = ({
|
||||
breadcrumbs,
|
||||
startIndex,
|
||||
onBreadcrumbClick,
|
||||
}: MenuProps) => {
|
||||
return (
|
||||
<div className='flex w-[136px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
|
||||
{breadcrumbs.map((breadcrumb, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={`${breadcrumb}-${index}`}
|
||||
name={breadcrumb}
|
||||
index={startIndex + index}
|
||||
onBreadcrumbClick={onBreadcrumbClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Menu)
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../../../../store'
|
||||
import Bucket from './bucket'
|
||||
import BreadcrumbItem from './item'
|
||||
import Dropdown from './dropdown'
|
||||
import Drive from './drive'
|
||||
|
||||
type BreadcrumbsProps = {
|
||||
breadcrumbs: string[]
|
||||
keywords: string
|
||||
bucket: string
|
||||
searchResultsLength: number
|
||||
isInPipeline: boolean
|
||||
}
|
||||
|
||||
const Breadcrumbs = ({
|
||||
breadcrumbs,
|
||||
keywords,
|
||||
bucket,
|
||||
searchResultsLength,
|
||||
isInPipeline,
|
||||
}: BreadcrumbsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
const hasBucket = useDataSourceStoreWithSelector(s => s.hasBucket)
|
||||
const showSearchResult = !!keywords && searchResultsLength > 0
|
||||
const showBucketListTitle = breadcrumbs.length === 0 && hasBucket && bucket === ''
|
||||
|
||||
const displayBreadcrumbNum = useMemo(() => {
|
||||
const num = isInPipeline ? 2 : 3
|
||||
return bucket ? num - 1 : num
|
||||
}, [isInPipeline, bucket])
|
||||
|
||||
const breadcrumbsConfig = useMemo(() => {
|
||||
const prefixToDisplay = breadcrumbs.slice(0, displayBreadcrumbNum - 1)
|
||||
const collapsedBreadcrumbs = breadcrumbs.slice(displayBreadcrumbNum - 1, breadcrumbs.length - 1)
|
||||
return {
|
||||
original: breadcrumbs,
|
||||
needCollapsed: breadcrumbs.length > displayBreadcrumbNum,
|
||||
prefixBreadcrumbs: prefixToDisplay,
|
||||
collapsedBreadcrumbs,
|
||||
lastBreadcrumb: breadcrumbs[breadcrumbs.length - 1],
|
||||
}
|
||||
}, [displayBreadcrumbNum, breadcrumbs])
|
||||
|
||||
const handleBackToBucketList = useCallback(() => {
|
||||
const { setOnlineDriveFileList, setSelectedFileIds, setBreadcrumbs, setPrefix, setBucket } = dataSourceStore.getState()
|
||||
setOnlineDriveFileList([])
|
||||
setSelectedFileIds([])
|
||||
setBucket('')
|
||||
setBreadcrumbs([])
|
||||
setPrefix([])
|
||||
}, [dataSourceStore])
|
||||
|
||||
const handleClickBucketName = useCallback(() => {
|
||||
const { setOnlineDriveFileList, setSelectedFileIds, setBreadcrumbs, setPrefix } = dataSourceStore.getState()
|
||||
setOnlineDriveFileList([])
|
||||
setSelectedFileIds([])
|
||||
setBreadcrumbs([])
|
||||
setPrefix([])
|
||||
}, [dataSourceStore])
|
||||
|
||||
const handleBackToRoot = useCallback(() => {
|
||||
const { setOnlineDriveFileList, setSelectedFileIds, setBreadcrumbs, setPrefix } = dataSourceStore.getState()
|
||||
setOnlineDriveFileList([])
|
||||
setSelectedFileIds([])
|
||||
setBreadcrumbs([])
|
||||
setPrefix([])
|
||||
}, [dataSourceStore])
|
||||
|
||||
const handleClickBreadcrumb = useCallback((index: number) => {
|
||||
const { breadcrumbs, prefix, setOnlineDriveFileList, setSelectedFileIds, setBreadcrumbs, setPrefix } = dataSourceStore.getState()
|
||||
const newBreadcrumbs = breadcrumbs.slice(0, index + 1)
|
||||
const newPrefix = prefix.slice(0, index + 1)
|
||||
setOnlineDriveFileList([])
|
||||
setSelectedFileIds([])
|
||||
setBreadcrumbs(newBreadcrumbs)
|
||||
setPrefix(newPrefix)
|
||||
}, [dataSourceStore])
|
||||
|
||||
return (
|
||||
<div className='flex grow items-center overflow-hidden'>
|
||||
{showSearchResult && (
|
||||
<div className='system-sm-medium text-test-secondary px-[5px]'>
|
||||
{t('datasetPipeline.onlineDrive.breadcrumbs.searchResult', {
|
||||
searchResultsLength,
|
||||
folderName: breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1] : bucket,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!showSearchResult && showBucketListTitle && (
|
||||
<div className='system-sm-medium text-test-secondary px-[5px]'>
|
||||
{t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
|
||||
</div>
|
||||
)}
|
||||
{!showSearchResult && !showBucketListTitle && (
|
||||
<div className='flex w-full items-center gap-x-0.5 overflow-hidden'>
|
||||
{hasBucket && bucket && (
|
||||
<Bucket
|
||||
bucketName={bucket}
|
||||
handleBackToBucketList={handleBackToBucketList}
|
||||
handleClickBucketName={handleClickBucketName}
|
||||
isActive={breadcrumbs.length === 0}
|
||||
disabled={breadcrumbs.length === 0}
|
||||
showSeparator={breadcrumbs.length > 0}
|
||||
/>
|
||||
)}
|
||||
{!hasBucket && (
|
||||
<Drive
|
||||
breadcrumbs={breadcrumbs}
|
||||
handleBackToRoot={handleBackToRoot}
|
||||
/>
|
||||
)}
|
||||
{!breadcrumbsConfig.needCollapsed && (
|
||||
<>
|
||||
{breadcrumbsConfig.original.map((breadcrumb, index) => {
|
||||
const isLast = index === breadcrumbsConfig.original.length - 1
|
||||
return (
|
||||
<BreadcrumbItem
|
||||
key={`${breadcrumb}-${index}`}
|
||||
index={index}
|
||||
handleClick={handleClickBreadcrumb}
|
||||
name={breadcrumb}
|
||||
isActive={isLast}
|
||||
showSeparator={!isLast}
|
||||
disabled={isLast}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{breadcrumbsConfig.needCollapsed && (
|
||||
<>
|
||||
{breadcrumbsConfig.prefixBreadcrumbs.map((breadcrumb, index) => {
|
||||
return (
|
||||
<BreadcrumbItem
|
||||
key={`${breadcrumb}-${index}`}
|
||||
index={index}
|
||||
handleClick={handleClickBreadcrumb}
|
||||
name={breadcrumb}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Dropdown
|
||||
startIndex={breadcrumbsConfig.prefixBreadcrumbs.length}
|
||||
breadcrumbs={breadcrumbsConfig.collapsedBreadcrumbs}
|
||||
onBreadcrumbClick={handleClickBreadcrumb}
|
||||
/>
|
||||
<BreadcrumbItem
|
||||
index={breadcrumbs.length - 1}
|
||||
handleClick={handleClickBreadcrumb}
|
||||
name={breadcrumbsConfig.lastBreadcrumb}
|
||||
isActive={true}
|
||||
disabled={true}
|
||||
showSeparator={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Breadcrumbs)
|
||||
@@ -0,0 +1,48 @@
|
||||
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(
|
||||
'max-w-full shrink truncate 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}
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
{showSeparator && <span className='system-xs-regular shrink-0 text-divider-deep'>/</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BreadcrumbItem.displayName = 'BreadcrumbItem'
|
||||
|
||||
export default React.memo(BreadcrumbItem)
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import Breadcrumbs from './breadcrumbs'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type HeaderProps = {
|
||||
breadcrumbs: string[]
|
||||
inputValue: string
|
||||
keywords: string
|
||||
bucket: string
|
||||
searchResultsLength: number
|
||||
handleInputChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
handleResetKeywords: () => void
|
||||
isInPipeline: boolean
|
||||
}
|
||||
|
||||
const Header = ({
|
||||
breadcrumbs,
|
||||
inputValue,
|
||||
keywords,
|
||||
bucket,
|
||||
isInPipeline,
|
||||
searchResultsLength,
|
||||
handleInputChange,
|
||||
handleResetKeywords,
|
||||
}: HeaderProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-2 bg-components-panel-bg p-1 pl-3'>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={breadcrumbs}
|
||||
keywords={keywords}
|
||||
bucket={bucket}
|
||||
searchResultsLength={searchResultsLength}
|
||||
isInPipeline={isInPipeline}
|
||||
/>
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onClear={handleResetKeywords}
|
||||
placeholder={t('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')}
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName='w-[200px] h-8 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -0,0 +1,82 @@
|
||||
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[]
|
||||
selectedFileIds: string[]
|
||||
breadcrumbs: string[]
|
||||
keywords: string
|
||||
bucket: string
|
||||
isInPipeline: boolean
|
||||
resetKeywords: () => void
|
||||
updateKeywords: (keywords: string) => void
|
||||
searchResultsLength: number
|
||||
handleSelectFile: (file: OnlineDriveFile) => void
|
||||
handleOpenFolder: (file: OnlineDriveFile) => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const FileList = ({
|
||||
fileList,
|
||||
selectedFileIds,
|
||||
breadcrumbs,
|
||||
keywords,
|
||||
bucket,
|
||||
resetKeywords,
|
||||
updateKeywords,
|
||||
searchResultsLength,
|
||||
handleSelectFile,
|
||||
handleOpenFolder,
|
||||
isInPipeline,
|
||||
isLoading,
|
||||
}: 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
|
||||
breadcrumbs={breadcrumbs}
|
||||
inputValue={inputValue}
|
||||
keywords={keywords}
|
||||
bucket={bucket}
|
||||
isInPipeline={isInPipeline}
|
||||
handleInputChange={handleInputChange}
|
||||
searchResultsLength={searchResultsLength}
|
||||
handleResetKeywords={handleResetKeywords}
|
||||
/>
|
||||
<List
|
||||
fileList={fileList}
|
||||
selectedFileIds={selectedFileIds}
|
||||
keywords={keywords}
|
||||
handleResetKeywords={handleResetKeywords}
|
||||
handleOpenFolder={handleOpenFolder}
|
||||
handleSelectFile={handleSelectFile}
|
||||
isInPipeline={isInPipeline}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileList
|
||||
@@ -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)
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { OnlineDriveFileType } from '@/models/pipeline'
|
||||
import { BucketsBlue, Folder } from '@/app/components/base/icons/src/public/knowledge/online-drive'
|
||||
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import { getFileType } from './utils'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type FileIconProps = {
|
||||
type: OnlineDriveFileType
|
||||
fileName: string
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const FileIcon = ({
|
||||
type,
|
||||
fileName,
|
||||
size = 'md',
|
||||
className,
|
||||
}: FileIconProps) => {
|
||||
const fileType = useMemo(() => {
|
||||
if (type === OnlineDriveFileType.bucket || type === OnlineDriveFileType.folder)
|
||||
return 'custom'
|
||||
|
||||
return getFileType(fileName)
|
||||
}, [type, fileName])
|
||||
|
||||
if (type === OnlineDriveFileType.bucket) {
|
||||
return (
|
||||
<BucketsBlue className={cn('size-[18px]', className)} />
|
||||
)
|
||||
}
|
||||
|
||||
if (type === OnlineDriveFileType.folder) {
|
||||
return (
|
||||
<Folder className={cn('size-[18px]', className)} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FileTypeIcon
|
||||
size={size}
|
||||
type={fileType}
|
||||
className={cn('size-[18px]', className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FileIcon)
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import Item from './item'
|
||||
import EmptyFolder from './empty-folder'
|
||||
import EmptySearchResult from './empty-search-result'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useDataSourceStore } from '../../../store'
|
||||
|
||||
type FileListProps = {
|
||||
fileList: OnlineDriveFile[]
|
||||
selectedFileIds: string[]
|
||||
keywords: string
|
||||
isInPipeline: boolean
|
||||
isLoading: boolean
|
||||
handleResetKeywords: () => void
|
||||
handleSelectFile: (file: OnlineDriveFile) => void
|
||||
handleOpenFolder: (file: OnlineDriveFile) => void
|
||||
}
|
||||
|
||||
const List = ({
|
||||
fileList,
|
||||
selectedFileIds,
|
||||
keywords,
|
||||
handleResetKeywords,
|
||||
handleSelectFile,
|
||||
handleOpenFolder,
|
||||
isInPipeline,
|
||||
isLoading,
|
||||
}: FileListProps) => {
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const observerRef = useRef<IntersectionObserver>(null)
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (anchorRef.current) {
|
||||
observerRef.current = new IntersectionObserver((entries) => {
|
||||
const { setNextPageParameters, currentNextPageParametersRef, isTruncated } = dataSourceStore.getState()
|
||||
if (entries[0].isIntersecting && isTruncated.current && !isLoading)
|
||||
setNextPageParameters(currentNextPageParametersRef.current)
|
||||
}, {
|
||||
rootMargin: '100px',
|
||||
})
|
||||
observerRef.current.observe(anchorRef.current)
|
||||
}
|
||||
return () => observerRef.current?.disconnect()
|
||||
}, [anchorRef, isLoading, dataSourceStore])
|
||||
|
||||
const isAllLoading = isLoading && fileList.length === 0 && keywords.length === 0
|
||||
const isPartialLoading = isLoading && fileList.length > 0
|
||||
const isEmptyFolder = !isLoading && fileList.length === 0 && keywords.length === 0
|
||||
const isSearchResultEmpty = !isLoading && fileList.length === 0 && keywords.length > 0
|
||||
|
||||
return (
|
||||
<div className='grow overflow-hidden p-1 pt-0'>
|
||||
{
|
||||
isAllLoading && (
|
||||
<Loading type='app' />
|
||||
)
|
||||
}
|
||||
{
|
||||
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 = selectedFileIds.includes(file.id)
|
||||
return (
|
||||
<Item
|
||||
key={file.id}
|
||||
file={file}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelectFile}
|
||||
onOpen={handleOpenFolder}
|
||||
isMultipleChoice={!isInPipeline}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
isPartialLoading && (
|
||||
<div className='flex items-center justify-center py-2'>
|
||||
<RiLoader2Line className='animation-spin size-4 text-text-tertiary' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div ref={anchorRef} className='h-0' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(List)
|
||||
@@ -0,0 +1,103 @@
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Radio from '@/app/components/base/radio/ui'
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import FileIcon from './file-icon'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Placement } from '@floating-ui/react'
|
||||
|
||||
type ItemProps = {
|
||||
file: OnlineDriveFile
|
||||
isSelected: boolean
|
||||
disabled?: boolean
|
||||
isMultipleChoice?: boolean
|
||||
onSelect: (file: OnlineDriveFile) => void
|
||||
onOpen: (file: OnlineDriveFile) => void
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
file,
|
||||
isSelected,
|
||||
disabled = false,
|
||||
isMultipleChoice = true,
|
||||
onSelect,
|
||||
onOpen,
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { id, name, type, size } = file
|
||||
|
||||
const isBucket = type === 'bucket'
|
||||
const isFolder = type === 'folder'
|
||||
|
||||
const Wrapper = disabled ? Tooltip : React.Fragment
|
||||
const wrapperProps = disabled ? {
|
||||
popupContent: t('datasetPipeline.onlineDrive.notSupportedFileType'),
|
||||
position: 'top-end' as Placement,
|
||||
offset: { mainAxis: 4, crossAxis: -104 },
|
||||
} : {}
|
||||
|
||||
const handleSelect = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
onSelect(file)
|
||||
}, [file, onSelect])
|
||||
|
||||
const handleClickItem = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
if (disabled) return
|
||||
if (isBucket || isFolder) {
|
||||
onOpen(file)
|
||||
return
|
||||
}
|
||||
onSelect(file)
|
||||
}, [disabled, file, isBucket, isFolder, onOpen, onSelect])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='flex cursor-pointer items-center gap-2 rounded-md px-2 py-[3px] hover:bg-state-base-hover'
|
||||
onClick={handleClickItem}
|
||||
>
|
||||
{!isBucket && isMultipleChoice && (
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
checked={isSelected}
|
||||
onCheck={handleSelect}
|
||||
/>
|
||||
)}
|
||||
{!isBucket && !isMultipleChoice && (
|
||||
<Radio
|
||||
className='shrink-0'
|
||||
disabled={disabled}
|
||||
isChecked={isSelected}
|
||||
onCheck={handleSelect}
|
||||
/>
|
||||
)}
|
||||
<Wrapper
|
||||
{...wrapperProps}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex grow items-center gap-x-1 overflow-hidden py-0.5',
|
||||
disabled && 'opacity-30',
|
||||
)}>
|
||||
<FileIcon type={type} fileName={name} className='shrink-0 transform-gpu' />
|
||||
<span
|
||||
className='system-sm-medium grow truncate text-text-secondary'
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{!isFolder && typeof size === 'number' && (
|
||||
<span className='system-xs-regular shrink-0 text-text-tertiary'>{formatFileSize(size)}</span>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Item)
|
||||
@@ -0,0 +1,51 @@
|
||||
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
|
||||
export const getFileExtension = (fileName: string): string => {
|
||||
if (!fileName)
|
||||
return ''
|
||||
const parts = fileName.split('.')
|
||||
if (parts.length <= 1 || (parts[0] === '' && parts.length === 2))
|
||||
return ''
|
||||
|
||||
return parts[parts.length - 1].toLowerCase()
|
||||
}
|
||||
|
||||
export const getFileType = (fileName: string) => {
|
||||
const extension = getFileExtension(fileName)
|
||||
|
||||
if (extension === 'gif')
|
||||
return FileAppearanceTypeEnum.gif
|
||||
|
||||
if (FILE_EXTS.image.includes(extension.toUpperCase()))
|
||||
return FileAppearanceTypeEnum.image
|
||||
|
||||
if (FILE_EXTS.video.includes(extension.toUpperCase()))
|
||||
return FileAppearanceTypeEnum.video
|
||||
|
||||
if (FILE_EXTS.audio.includes(extension.toUpperCase()))
|
||||
return FileAppearanceTypeEnum.audio
|
||||
|
||||
if (extension === 'html' || extension === 'htm' || extension === 'xml' || extension === 'json')
|
||||
return FileAppearanceTypeEnum.code
|
||||
|
||||
if (extension === 'pdf')
|
||||
return FileAppearanceTypeEnum.pdf
|
||||
|
||||
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
|
||||
return FileAppearanceTypeEnum.markdown
|
||||
|
||||
if (extension === 'xlsx' || extension === 'xls' || extension === 'csv')
|
||||
return FileAppearanceTypeEnum.excel
|
||||
|
||||
if (extension === 'docx' || extension === 'doc')
|
||||
return FileAppearanceTypeEnum.word
|
||||
|
||||
if (extension === 'pptx' || extension === 'ppt')
|
||||
return FileAppearanceTypeEnum.ppt
|
||||
|
||||
if (FILE_EXTS.document.includes(extension.toUpperCase()))
|
||||
return FileAppearanceTypeEnum.document
|
||||
|
||||
return FileAppearanceTypeEnum.custom
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react'
|
||||
|
||||
type HeaderProps = {
|
||||
onClickConfiguration?: () => void
|
||||
docTitle: string
|
||||
docLink: string
|
||||
}
|
||||
|
||||
const Header = ({
|
||||
onClickConfiguration,
|
||||
docTitle,
|
||||
docLink,
|
||||
}: HeaderProps) => {
|
||||
return (
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<div className='flex shrink-0 grow items-center gap-x-1'>
|
||||
<div className='w-20 bg-black'>
|
||||
{/* placeholder */}
|
||||
</div>
|
||||
<Divider type='vertical' className='mx-1 h-3.5' />
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
className='px-1'
|
||||
>
|
||||
<RiEqualizer2Line
|
||||
className='size-4'
|
||||
onClick={onClickConfiguration}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<a
|
||||
className='system-xs-medium flex items-center gap-x-1 overflow-hidden text-text-accent'
|
||||
href={docLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<RiBookOpenLine className='size-3.5 shrink-0' />
|
||||
<span className='grow truncate' title={docTitle}>{docTitle}</span>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -0,0 +1,217 @@
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import Header from '../base/header'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import FileList from './file-list'
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { ssePost } from '@/service/base'
|
||||
import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
|
||||
import { convertOnlineDriveData } from './utils'
|
||||
import { produce } from 'immer'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useGetDataSourceAuth } from '@/service/use-datasource'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
|
||||
type OnlineDriveProps = {
|
||||
nodeId: string
|
||||
nodeData: DataSourceNodeType
|
||||
isInPipeline?: boolean
|
||||
onCredentialChange: (credentialId: string) => void
|
||||
}
|
||||
|
||||
const OnlineDrive = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
isInPipeline = false,
|
||||
onCredentialChange,
|
||||
}: OnlineDriveProps) => {
|
||||
const docLink = useDocLink()
|
||||
const [isInitialMount, setIsInitialMount] = useState(true)
|
||||
const pipelineId = useDatasetDetailContextWithSelector(s => s.dataset?.pipeline_id)
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
const {
|
||||
nextPageParameters,
|
||||
breadcrumbs,
|
||||
prefix,
|
||||
keywords,
|
||||
bucket,
|
||||
selectedFileIds,
|
||||
onlineDriveFileList,
|
||||
currentCredentialId,
|
||||
} = useDataSourceStoreWithSelector(useShallow(state => ({
|
||||
nextPageParameters: state.nextPageParameters,
|
||||
breadcrumbs: state.breadcrumbs,
|
||||
prefix: state.prefix,
|
||||
keywords: state.keywords,
|
||||
bucket: state.bucket,
|
||||
selectedFileIds: state.selectedFileIds,
|
||||
onlineDriveFileList: state.onlineDriveFileList,
|
||||
currentCredentialId: state.currentCredentialId,
|
||||
})))
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const isLoadingRef = useRef(false)
|
||||
|
||||
const { data: dataSourceAuth } = useGetDataSourceAuth({
|
||||
pluginId: nodeData.plugin_id,
|
||||
provider: nodeData.provider_name,
|
||||
})
|
||||
|
||||
const datasourceNodeRunURL = !isInPipeline
|
||||
? `/rag/pipelines/${pipelineId}/workflows/published/datasource/nodes/${nodeId}/run`
|
||||
: `/rag/pipelines/${pipelineId}/workflows/draft/datasource/nodes/${nodeId}/run`
|
||||
|
||||
const getOnlineDriveFiles = useCallback(async () => {
|
||||
if (isLoadingRef.current) return
|
||||
const { nextPageParameters, prefix, bucket, onlineDriveFileList, currentCredentialId } = dataSourceStore.getState()
|
||||
setIsLoading(true)
|
||||
isLoadingRef.current = true
|
||||
ssePost(
|
||||
datasourceNodeRunURL,
|
||||
{
|
||||
body: {
|
||||
inputs: {
|
||||
prefix: prefix[prefix.length - 1],
|
||||
bucket,
|
||||
next_page_parameters: nextPageParameters,
|
||||
max_keys: 30,
|
||||
},
|
||||
datasource_type: DatasourceType.onlineDrive,
|
||||
credential_id: currentCredentialId,
|
||||
},
|
||||
},
|
||||
{
|
||||
onDataSourceNodeCompleted: (documentsData: DataSourceNodeCompletedResponse) => {
|
||||
const { setOnlineDriveFileList, isTruncated, currentNextPageParametersRef, setHasBucket } = dataSourceStore.getState()
|
||||
const {
|
||||
fileList: newFileList,
|
||||
isTruncated: newIsTruncated,
|
||||
nextPageParameters: newNextPageParameters,
|
||||
hasBucket: newHasBucket,
|
||||
} = convertOnlineDriveData(documentsData.data, breadcrumbs, bucket)
|
||||
setOnlineDriveFileList([...onlineDriveFileList, ...newFileList])
|
||||
isTruncated.current = newIsTruncated
|
||||
currentNextPageParametersRef.current = newNextPageParameters
|
||||
setHasBucket(newHasBucket)
|
||||
setIsLoading(false)
|
||||
isLoadingRef.current = false
|
||||
},
|
||||
onDataSourceNodeError: (error: DataSourceNodeErrorResponse) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error.error,
|
||||
})
|
||||
setIsLoading(false)
|
||||
isLoadingRef.current = false
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [datasourceNodeRunURL, dataSourceStore])
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentCredentialId) return
|
||||
if (isInitialMount) {
|
||||
// Only fetch files on initial mount if fileList is empty
|
||||
if (onlineDriveFileList.length === 0)
|
||||
getOnlineDriveFiles()
|
||||
setIsInitialMount(false)
|
||||
}
|
||||
else {
|
||||
getOnlineDriveFiles()
|
||||
}
|
||||
}, [nextPageParameters, prefix, bucket, currentCredentialId])
|
||||
|
||||
const filteredOnlineDriveFileList = useMemo(() => {
|
||||
if (keywords)
|
||||
return onlineDriveFileList.filter(file => file.name.toLowerCase().includes(keywords.toLowerCase()))
|
||||
return onlineDriveFileList
|
||||
}, [onlineDriveFileList, keywords])
|
||||
|
||||
const updateKeywords = useCallback((keywords: string) => {
|
||||
const { setKeywords } = dataSourceStore.getState()
|
||||
setKeywords(keywords)
|
||||
}, [dataSourceStore])
|
||||
|
||||
const resetKeywords = useCallback(() => {
|
||||
const { setKeywords } = dataSourceStore.getState()
|
||||
|
||||
setKeywords('')
|
||||
}, [dataSourceStore])
|
||||
|
||||
const handleSelectFile = useCallback((file: OnlineDriveFile) => {
|
||||
const { selectedFileIds, setSelectedFileIds } = dataSourceStore.getState()
|
||||
if (file.type === OnlineDriveFileType.bucket) return
|
||||
const newSelectedFileList = produce(selectedFileIds, (draft) => {
|
||||
if (draft.includes(file.id)) {
|
||||
const index = draft.indexOf(file.id)
|
||||
draft.splice(index, 1)
|
||||
}
|
||||
else {
|
||||
if (isInPipeline && draft.length >= 1) return
|
||||
draft.push(file.id)
|
||||
}
|
||||
})
|
||||
setSelectedFileIds(newSelectedFileList)
|
||||
}, [dataSourceStore, isInPipeline])
|
||||
|
||||
const handleOpenFolder = useCallback((file: OnlineDriveFile) => {
|
||||
const { breadcrumbs, prefix, setBreadcrumbs, setPrefix, setBucket, setOnlineDriveFileList, setSelectedFileIds } = dataSourceStore.getState()
|
||||
if (file.type === OnlineDriveFileType.file) return
|
||||
setOnlineDriveFileList([])
|
||||
if (file.type === OnlineDriveFileType.bucket) {
|
||||
setBucket(file.name)
|
||||
}
|
||||
else {
|
||||
setSelectedFileIds([])
|
||||
const newBreadcrumbs = produce(breadcrumbs, (draft) => {
|
||||
draft.push(file.name)
|
||||
})
|
||||
const newPrefix = produce(prefix, (draft) => {
|
||||
draft.push(file.id)
|
||||
})
|
||||
setBreadcrumbs(newBreadcrumbs)
|
||||
setPrefix(newPrefix)
|
||||
}
|
||||
}, [dataSourceStore, getOnlineDriveFiles])
|
||||
|
||||
const handleSetting = useCallback(() => {
|
||||
setShowAccountSettingModal({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [setShowAccountSettingModal])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-y-2'>
|
||||
<Header
|
||||
docTitle='Docs'
|
||||
docLink={docLink('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')}
|
||||
onClickConfiguration={handleSetting}
|
||||
pluginName={nodeData.datasource_label}
|
||||
currentCredentialId={currentCredentialId}
|
||||
onCredentialChange={onCredentialChange}
|
||||
credentials={dataSourceAuth?.result || []}
|
||||
/>
|
||||
<FileList
|
||||
fileList={filteredOnlineDriveFileList}
|
||||
selectedFileIds={selectedFileIds}
|
||||
breadcrumbs={breadcrumbs}
|
||||
keywords={keywords}
|
||||
bucket={bucket}
|
||||
resetKeywords={resetKeywords}
|
||||
updateKeywords={updateKeywords}
|
||||
searchResultsLength={filteredOnlineDriveFileList.length}
|
||||
handleSelectFile={handleSelectFile}
|
||||
handleOpenFolder={handleOpenFolder}
|
||||
isInPipeline={isInPipeline}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnlineDrive
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type OnlineDriveFile, OnlineDriveFileType } from '@/models/pipeline'
|
||||
import type { OnlineDriveData } from '@/types/pipeline'
|
||||
|
||||
export const isFile = (type: 'file' | 'folder'): boolean => {
|
||||
return type === 'file'
|
||||
}
|
||||
|
||||
export const isBucketListInitiation = (data: OnlineDriveData[], prefix: string[], bucket: string): boolean => {
|
||||
if (bucket || prefix.length > 0) return false
|
||||
const hasBucket = data.every(item => !!item.bucket)
|
||||
return hasBucket && (data.length > 1 || (data.length === 1 && !!data[0].bucket && data[0].files.length === 0))
|
||||
}
|
||||
|
||||
export const convertOnlineDriveData = (data: OnlineDriveData[], prefix: string[], bucket: string): {
|
||||
fileList: OnlineDriveFile[],
|
||||
isTruncated: boolean,
|
||||
nextPageParameters: Record<string, any>
|
||||
hasBucket: boolean
|
||||
} => {
|
||||
const fileList: OnlineDriveFile[] = []
|
||||
let isTruncated = false
|
||||
let nextPageParameters: Record<string, any> = {}
|
||||
let hasBucket = false
|
||||
|
||||
if (data.length === 0)
|
||||
return { fileList, isTruncated, nextPageParameters, hasBucket }
|
||||
|
||||
if (isBucketListInitiation(data, prefix, bucket)) {
|
||||
data.forEach((item) => {
|
||||
fileList.push({
|
||||
id: item.bucket,
|
||||
name: item.bucket,
|
||||
type: OnlineDriveFileType.bucket,
|
||||
})
|
||||
})
|
||||
hasBucket = true
|
||||
}
|
||||
else {
|
||||
data[0].files.forEach((file) => {
|
||||
const { id, name, size, type } = file
|
||||
const isFileType = isFile(type)
|
||||
fileList.push({
|
||||
id,
|
||||
name,
|
||||
size: isFileType ? size : undefined,
|
||||
type: isFileType ? OnlineDriveFileType.file : OnlineDriveFileType.folder,
|
||||
})
|
||||
})
|
||||
isTruncated = data[0].is_truncated ?? false
|
||||
nextPageParameters = data[0].next_page_parameters ?? {}
|
||||
hasBucket = !!data[0].bucket
|
||||
}
|
||||
return { fileList, isTruncated, nextPageParameters, hasBucket }
|
||||
}
|
||||
Reference in New Issue
Block a user