Files
dify/web/app/components/workflow/skill/file-tree/artifacts-tree.tsx
yyh ab52550abe feat(sandbox): use extension field for file icon type mapping
Enhance getFileIconType to accept an extension parameter and cover all
13 FileAppearanceTypeEnum types using an O(1) Map lookup. Update all
call sites to pass the API-provided extension for accurate icon display.
2026-01-27 16:21:03 +08:00

163 lines
4.8 KiB
TypeScript

'use client'
import type { FC } from 'react'
import type { FileAppearanceType } from '@/app/components/base/file-uploader/types'
import type { SandboxFileTreeNode } from '@/types/sandbox-file'
import { RiDownloadLine, RiFolderLine, RiFolderOpenLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useState } from 'react'
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
import { cn } from '@/utils/classnames'
import { getFileIconType } from '../utils/file-utils'
import TreeGuideLines from './tree-guide-lines'
const INDENT_SIZE = 20
type ArtifactsTreeProps = {
data: SandboxFileTreeNode[] | undefined
onDownload: (node: SandboxFileTreeNode) => void
onSelect?: (node: SandboxFileTreeNode) => void
selectedPath?: string
isDownloading?: boolean
}
type ArtifactsTreeNodeProps = {
node: SandboxFileTreeNode
depth: number
onDownload: (node: SandboxFileTreeNode) => void
onSelect?: (node: SandboxFileTreeNode) => void
selectedPath?: string
isDownloading?: boolean
}
const ArtifactsTreeNode: FC<ArtifactsTreeNodeProps> = ({
node,
depth,
onDownload,
onSelect,
selectedPath,
isDownloading,
}) => {
const [isExpanded, setIsExpanded] = useState(false)
const isFolder = node.node_type === 'folder'
const hasChildren = isFolder && node.children.length > 0
const isSelected = !isFolder && selectedPath === node.path
const handleClick = useCallback(() => {
if (isFolder) {
setIsExpanded(prev => !prev)
}
else {
onSelect?.(node)
}
}, [isFolder, node, onSelect])
const handleDownload = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
onDownload(node)
}, [node, onDownload])
const fileIconType = !isFolder ? getFileIconType(node.name, node.extension) : null
return (
<div>
<div
role="button"
tabIndex={0}
aria-label={isFolder ? `${node.name} folder` : node.name}
aria-expanded={isFolder ? isExpanded : undefined}
aria-selected={isSelected}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ')
handleClick()
}}
className={cn(
'group relative flex h-6 cursor-pointer items-center rounded-md px-2',
'hover:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-components-input-border-active',
isSelected && 'bg-state-base-hover',
)}
style={{ paddingLeft: `${8 + depth * INDENT_SIZE}px` }}
>
<TreeGuideLines level={depth} lineOffset={2} />
<div className="flex size-5 shrink-0 items-center justify-center">
{isFolder
? (
isExpanded
? <RiFolderOpenLine className="size-4 text-text-accent" aria-hidden="true" />
: <RiFolderLine className="size-4 text-text-secondary" aria-hidden="true" />
)
: <FileTypeIcon type={fileIconType as FileAppearanceType} size="sm" />}
</div>
<span className="min-w-0 flex-1 truncate text-[13px] font-normal leading-4 text-text-secondary">
{node.name}
</span>
{!isFolder && (
<button
type="button"
onClick={handleDownload}
disabled={isDownloading}
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded opacity-0 group-hover:opacity-100',
'hover:bg-state-base-hover-alt',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-components-input-border-active',
'disabled:cursor-not-allowed disabled:opacity-50',
)}
aria-label={`Download ${node.name}`}
>
<RiDownloadLine className="size-3.5 text-text-tertiary" />
</button>
)}
</div>
{isFolder && isExpanded && hasChildren && (
<div>
{node.children.map(child => (
<ArtifactsTreeNode
key={child.id}
node={child}
depth={depth + 1}
onDownload={onDownload}
onSelect={onSelect}
selectedPath={selectedPath}
isDownloading={isDownloading}
/>
))}
</div>
)}
</div>
)
}
const ArtifactsTree: FC<ArtifactsTreeProps> = ({
data,
onDownload,
onSelect,
selectedPath,
isDownloading,
}) => {
if (!data || data.length === 0)
return null
return (
<div className="py-0.5">
{data.map(node => (
<ArtifactsTreeNode
key={node.id}
node={node}
depth={0}
onDownload={onDownload}
onSelect={onSelect}
selectedPath={selectedPath}
isDownloading={isDownloading}
/>
))}
</div>
)
}
export default React.memo(ArtifactsTree)