mirror of
https://github.com/langgenius/dify.git
synced 2026-01-19 11:45:05 +08:00
feat(skill): add inline rename and guide lines to file tree
Add TreeEditInput component for inline file/folder renaming with keyboard support (Enter to submit, Escape to cancel). Add TreeGuideLines component to render vertical indent lines based on node depth for better visual hierarchy in the tree view. Reorganize file tree components into dedicated `file-tree` subdirectory for better code organization.
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { NodeApi, TreeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from './type'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEdit2Line,
|
||||
@ -11,7 +11,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useFileOperations } from './hooks/use-file-operations'
|
||||
import { useFileOperations } from '../hooks/use-file-operations'
|
||||
|
||||
type MenuItemProps = {
|
||||
icon: React.ElementType
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { NodeApi, TreeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from './type'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEdit2Line,
|
||||
@ -15,7 +15,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useFileOperations } from './hooks/use-file-operations'
|
||||
import { useFileOperations } from '../hooks/use-file-operations'
|
||||
|
||||
type MenuItemProps = {
|
||||
icon: React.ElementType
|
||||
@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import type { NodeApi, TreeApi } from 'react-arborist'
|
||||
import type { OpensObject } from './store'
|
||||
import type { TreeNodeData } from './type'
|
||||
import type { OpensObject } from '../store'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import { RiDragDropLine } from '@remixicon/react'
|
||||
import { useIsMutating } from '@tanstack/react-query'
|
||||
import { useSize } from 'ahooks'
|
||||
@ -15,11 +15,11 @@ import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useRenameAppAssetNode } from '@/service/use-app-asset'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree'
|
||||
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
||||
import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree'
|
||||
import { useSkillEditorStore, useSkillEditorStoreApi } from '../store'
|
||||
import { getAncestorIds } from '../utils/tree-utils'
|
||||
import TreeContextMenu from './tree-context-menu'
|
||||
import TreeNode from './tree-node'
|
||||
import { getAncestorIds } from './utils/tree-utils'
|
||||
|
||||
type FileTreeProps = {
|
||||
className?: string
|
||||
@ -2,15 +2,15 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { TreeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from './type'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree'
|
||||
import { useSkillEditorStore, useSkillEditorStoreApi } from '../store'
|
||||
import { findNodeById } from '../utils/tree-utils'
|
||||
import FileNodeMenu from './file-node-menu'
|
||||
import FolderNodeMenu from './folder-node-menu'
|
||||
import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree'
|
||||
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
||||
import { findNodeById } from './utils/tree-utils'
|
||||
|
||||
type TreeContextMenuProps = {
|
||||
treeRef: React.RefObject<TreeApi<TreeNodeData> | null>
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
import type { NodeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type TreeEditInputProps = {
|
||||
node: NodeApi<TreeNodeData>
|
||||
}
|
||||
|
||||
const TreeEditInput: React.FC<TreeEditInputProps> = ({ node }) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.select()
|
||||
}, [])
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
node.reset()
|
||||
}
|
||||
else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
node.submit(inputRef.current?.value || '')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
node.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
defaultValue={node.data.name}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="min-w-0 flex-1 rounded border border-components-input-border-active bg-transparent px-1 text-[13px] font-normal leading-4 text-text-primary outline-none"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TreeEditInput)
|
||||
@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
const INDENT_SIZE = 20
|
||||
|
||||
type TreeGuideLinesProps = {
|
||||
level: number
|
||||
}
|
||||
|
||||
const TreeGuideLines: React.FC<TreeGuideLinesProps> = ({ level }) => {
|
||||
if (level === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: level }).map((_, i) => (
|
||||
<div
|
||||
key={`guide-${i}`}
|
||||
className="absolute bottom-0 top-0 border-l border-divider-subtle"
|
||||
style={{ left: `${(i + 1) * INDENT_SIZE - 10}px` }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TreeGuideLines)
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { NodeRendererProps } from 'react-arborist'
|
||||
import type { TreeNodeData } from './type'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import type { FileAppearanceType } from '@/app/components/base/file-uploader/types'
|
||||
import { RiFolderLine, RiFolderOpenLine, RiMoreFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
@ -14,10 +14,12 @@ import {
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useSkillEditorStore, useSkillEditorStoreApi } from '../store'
|
||||
import { getFileIconType } from '../utils/file-utils'
|
||||
import FileNodeMenu from './file-node-menu'
|
||||
import FolderNodeMenu from './folder-node-menu'
|
||||
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
||||
import { getFileIconType } from './utils/file-utils'
|
||||
import TreeEditInput from './tree-edit-input'
|
||||
import TreeGuideLines from './tree-guide-lines'
|
||||
|
||||
const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
@ -83,7 +85,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
aria-selected={isSelected}
|
||||
aria-expanded={isFolder ? node.isOpen : undefined}
|
||||
className={cn(
|
||||
'group flex h-6 cursor-pointer items-center gap-2 rounded-md px-2',
|
||||
'group relative flex h-6 cursor-pointer items-center gap-2 rounded-md px-2',
|
||||
'hover:bg-state-base-hover',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
|
||||
isSelected && 'bg-state-base-active',
|
||||
@ -94,6 +96,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
onKeyDown={handleKeyDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<TreeGuideLines level={node.level} />
|
||||
<div className="flex size-5 shrink-0 items-center justify-center">
|
||||
{isFolder
|
||||
? (
|
||||
@ -122,16 +125,22 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate text-[13px] font-normal leading-4',
|
||||
isSelected
|
||||
? 'text-text-primary'
|
||||
: 'text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{node.data.name}
|
||||
</span>
|
||||
{node.isEditing
|
||||
? (
|
||||
<TreeEditInput node={node} />
|
||||
)
|
||||
: (
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate text-[13px] font-normal leading-4',
|
||||
isSelected
|
||||
? 'text-text-primary'
|
||||
: 'text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{node.data.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<PortalToFollowElem
|
||||
placement="bottom-start"
|
||||
Reference in New Issue
Block a user