mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 08:58:09 +08:00
refactor(skill): split file operations hook and extract TreeNodeIcon component
Split use-file-operations.ts (248 lines) into smaller focused hooks: - use-create-operations.ts for file/folder creation and upload - use-modify-operations.ts for rename and delete operations - use-file-operations.ts now serves as orchestrator maintaining backward compatibility Extract TreeNodeIcon component from tree-node.tsx for cleaner separation of concerns. Add brief comments to drag hooks explaining their purpose and relationships.
This commit is contained in:
@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
|
||||
// Icon rendering for tree nodes (folder/file icons with dirty indicator)
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FileAppearanceType } from '@/app/components/base/file-uploader/types'
|
||||
import { RiFolderLine, RiFolderOpenLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { getFileIconType } from '../utils/file-utils'
|
||||
|
||||
type TreeNodeIconProps = {
|
||||
isFolder: boolean
|
||||
isOpen: boolean
|
||||
fileName: string
|
||||
isDirty: boolean
|
||||
onToggle?: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
export const TreeNodeIcon: FC<TreeNodeIconProps> = ({
|
||||
isFolder,
|
||||
isOpen,
|
||||
fileName,
|
||||
isDirty,
|
||||
onToggle,
|
||||
}) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
|
||||
if (isFolder) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={onToggle}
|
||||
aria-label={t('skillSidebar.toggleFolder')}
|
||||
className={cn(
|
||||
'flex size-full items-center justify-center rounded',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-active',
|
||||
)}
|
||||
>
|
||||
{isOpen
|
||||
? <RiFolderOpenLine className="size-4 text-text-accent" aria-hidden="true" />
|
||||
: <RiFolderLine className="size-4 text-text-secondary" aria-hidden="true" />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const fileIconType = getFileIconType(fileName)
|
||||
|
||||
return (
|
||||
<div className="relative flex size-full items-center justify-center">
|
||||
<FileTypeIcon type={fileIconType as FileAppearanceType} size="sm" />
|
||||
{isDirty && (
|
||||
<span className="absolute -bottom-px -right-px size-[7px] rounded-full border border-white bg-text-warning-secondary" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -2,12 +2,10 @@
|
||||
|
||||
import type { NodeRendererProps } from 'react-arborist'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import type { FileAppearanceType } from '@/app/components/base/file-uploader/types'
|
||||
import { RiFolderLine, RiFolderOpenLine, RiMoreFill } from '@remixicon/react'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
@ -17,10 +15,10 @@ import { useStore } from '@/app/components/workflow/store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useFolderFileDrop } from '../hooks/use-folder-file-drop'
|
||||
import { useTreeNodeHandlers } from '../hooks/use-tree-node-handlers'
|
||||
import { getFileIconType } from '../utils/file-utils'
|
||||
import NodeMenu from './node-menu'
|
||||
import TreeEditInput from './tree-edit-input'
|
||||
import TreeGuideLines from './tree-guide-lines'
|
||||
import { TreeNodeIcon } from './tree-node-icon'
|
||||
|
||||
const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
@ -32,8 +30,6 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
|
||||
const fileIconType = !isFolder ? getFileIconType(node.data.name) : null
|
||||
|
||||
const {
|
||||
handleClick,
|
||||
handleDoubleClick,
|
||||
@ -85,31 +81,13 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<div className="flex size-5 shrink-0 items-center justify-center">
|
||||
{isFolder
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={handleToggle}
|
||||
aria-label={t('skillSidebar.toggleFolder')}
|
||||
className={cn(
|
||||
'flex size-full items-center justify-center rounded',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-active',
|
||||
)}
|
||||
>
|
||||
{node.isOpen
|
||||
? <RiFolderOpenLine className="size-4 text-text-accent" aria-hidden="true" />
|
||||
: <RiFolderLine className="size-4 text-text-secondary" aria-hidden="true" />}
|
||||
</button>
|
||||
)
|
||||
: (
|
||||
<div className="relative flex size-full items-center justify-center">
|
||||
<FileTypeIcon type={fileIconType as FileAppearanceType} size="sm" />
|
||||
{isDirty && (
|
||||
<span className="absolute -bottom-px -right-px size-[7px] rounded-full border border-white bg-text-warning-secondary" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<TreeNodeIcon
|
||||
isFolder={isFolder}
|
||||
isOpen={node.isOpen}
|
||||
fileName={node.data.name}
|
||||
isDirty={isDirty}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{node.isEditing
|
||||
|
||||
166
web/app/components/workflow/skill/hooks/use-create-operations.ts
Normal file
166
web/app/components/workflow/skill/hooks/use-create-operations.ts
Normal file
@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
// Handles file/folder creation and upload operations
|
||||
|
||||
import type { StoreApi } from 'zustand'
|
||||
import type { SkillEditorSliceShape } from '@/app/components/workflow/store/workflow/skill-editor/types'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
useCreateAppAssetFile,
|
||||
useCreateAppAssetFolder,
|
||||
} from '@/service/use-app-asset'
|
||||
|
||||
type UseCreateOperationsOptions = {
|
||||
parentId: string | null
|
||||
appId: string
|
||||
storeApi: StoreApi<SkillEditorSliceShape>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function useCreateOperations({
|
||||
parentId,
|
||||
appId,
|
||||
storeApi,
|
||||
onClose,
|
||||
}: UseCreateOperationsOptions) {
|
||||
const { t } = useTranslation('workflow')
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const folderInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const createFolder = useCreateAppAssetFolder()
|
||||
const createFile = useCreateAppAssetFile()
|
||||
|
||||
const handleNewFile = useCallback(() => {
|
||||
storeApi.getState().startCreateNode('file', parentId)
|
||||
onClose()
|
||||
}, [onClose, parentId, storeApi])
|
||||
|
||||
const handleNewFolder = useCallback(() => {
|
||||
storeApi.getState().startCreateNode('folder', parentId)
|
||||
onClose()
|
||||
}, [onClose, parentId, storeApi])
|
||||
|
||||
const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length === 0) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
await createFile.mutateAsync({
|
||||
appId,
|
||||
name: file.name,
|
||||
file,
|
||||
parentId,
|
||||
})
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.filesUploaded', { count: files.length }),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.uploadError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, createFile, onClose, parentId, t])
|
||||
|
||||
const handleFolderChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length === 0) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const folders = new Set<string>()
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name
|
||||
const parts = relativePath.split('/')
|
||||
|
||||
if (parts.length > 1) {
|
||||
let folderPath = ''
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
folderPath = folderPath ? `${folderPath}/${parts[i]}` : parts[i]
|
||||
folders.add(folderPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedFolders = Array.from(folders).sort((a, b) => {
|
||||
return a.split('/').length - b.split('/').length
|
||||
})
|
||||
|
||||
const folderIdMap = new Map<string, string | null>()
|
||||
folderIdMap.set('', parentId)
|
||||
|
||||
for (const folderPath of sortedFolders) {
|
||||
const parts = folderPath.split('/')
|
||||
const folderName = parts[parts.length - 1]
|
||||
const parentPath = parts.slice(0, -1).join('/')
|
||||
const parentFolderId = folderIdMap.get(parentPath) ?? parentId
|
||||
|
||||
const result = await createFolder.mutateAsync({
|
||||
appId,
|
||||
payload: {
|
||||
name: folderName,
|
||||
parent_id: parentFolderId,
|
||||
},
|
||||
})
|
||||
|
||||
folderIdMap.set(folderPath, result.id)
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name
|
||||
const parts = relativePath.split('/')
|
||||
const parentPath = parts.length > 1 ? parts.slice(0, -1).join('/') : ''
|
||||
const targetParentId = folderIdMap.get(parentPath) ?? parentId
|
||||
|
||||
await createFile.mutateAsync({
|
||||
appId,
|
||||
name: file.name,
|
||||
file,
|
||||
parentId: targetParentId,
|
||||
})
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.folderUploaded'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.uploadError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, createFile, createFolder, onClose, parentId, t])
|
||||
|
||||
return {
|
||||
fileInputRef,
|
||||
folderInputRef,
|
||||
isCreating: createFile.isPending || createFolder.isPending,
|
||||
handleNewFile,
|
||||
handleNewFolder,
|
||||
handleFileChange,
|
||||
handleFolderChange,
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
'use client'
|
||||
|
||||
// Base drag-and-drop handler for file uploads
|
||||
// Used by use-root-file-drop and use-folder-file-drop
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
'use client'
|
||||
|
||||
// Orchestrator hook for file operations - combines create and modify operations
|
||||
// Maintains backward compatibility for existing consumers
|
||||
|
||||
import type { NodeApi, TreeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useCreateAppAssetFile,
|
||||
useCreateAppAssetFolder,
|
||||
useDeleteAppAssetNode,
|
||||
} from '@/service/use-app-asset'
|
||||
import { getAllDescendantFileIds } from '../utils/tree-utils'
|
||||
import { useCreateOperations } from './use-create-operations'
|
||||
import { useModifyOperations } from './use-modify-operations'
|
||||
import { useSkillAssetTreeData } from './use-skill-asset-tree'
|
||||
|
||||
type UseFileOperationsOptions = {
|
||||
@ -29,219 +25,49 @@ export function useFileOperations({
|
||||
node,
|
||||
}: UseFileOperationsOptions) {
|
||||
const nodeId = node?.data.id ?? explicitNodeId ?? ''
|
||||
const { t } = useTranslation('workflow')
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const folderInputRef = useRef<HTMLInputElement>(null)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const storeApi = useWorkflowStore()
|
||||
|
||||
const createFolder = useCreateAppAssetFolder()
|
||||
const createFile = useCreateAppAssetFile()
|
||||
const deleteNode = useDeleteAppAssetNode()
|
||||
const { data: treeData } = useSkillAssetTreeData()
|
||||
|
||||
const parentId = nodeId === 'root' ? null : nodeId
|
||||
|
||||
const handleNewFile = useCallback(() => {
|
||||
storeApi.getState().startCreateNode('file', parentId)
|
||||
onClose()
|
||||
}, [onClose, parentId, storeApi])
|
||||
const createOps = useCreateOperations({
|
||||
parentId,
|
||||
appId,
|
||||
storeApi,
|
||||
onClose,
|
||||
})
|
||||
|
||||
const handleNewFolder = useCallback(() => {
|
||||
storeApi.getState().startCreateNode('folder', parentId)
|
||||
onClose()
|
||||
}, [onClose, parentId, storeApi])
|
||||
|
||||
const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length === 0) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
await createFile.mutateAsync({
|
||||
appId,
|
||||
name: file.name,
|
||||
file,
|
||||
parentId,
|
||||
})
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.filesUploaded', { count: files.length }),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.uploadError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, createFile, onClose, parentId, t])
|
||||
|
||||
const handleFolderChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length === 0) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const folders = new Set<string>()
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name
|
||||
const parts = relativePath.split('/')
|
||||
|
||||
if (parts.length > 1) {
|
||||
let folderPath = ''
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
folderPath = folderPath ? `${folderPath}/${parts[i]}` : parts[i]
|
||||
folders.add(folderPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedFolders = Array.from(folders).sort((a, b) => {
|
||||
return a.split('/').length - b.split('/').length
|
||||
})
|
||||
|
||||
const folderIdMap = new Map<string, string | null>()
|
||||
folderIdMap.set('', parentId)
|
||||
|
||||
for (const folderPath of sortedFolders) {
|
||||
const parts = folderPath.split('/')
|
||||
const folderName = parts[parts.length - 1]
|
||||
const parentPath = parts.slice(0, -1).join('/')
|
||||
const parentFolderId = folderIdMap.get(parentPath) ?? parentId
|
||||
|
||||
const result = await createFolder.mutateAsync({
|
||||
appId,
|
||||
payload: {
|
||||
name: folderName,
|
||||
parent_id: parentFolderId,
|
||||
},
|
||||
})
|
||||
|
||||
folderIdMap.set(folderPath, result.id)
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name
|
||||
const parts = relativePath.split('/')
|
||||
const parentPath = parts.length > 1 ? parts.slice(0, -1).join('/') : ''
|
||||
const targetParentId = folderIdMap.get(parentPath) ?? parentId
|
||||
|
||||
await createFile.mutateAsync({
|
||||
appId,
|
||||
name: file.name,
|
||||
file,
|
||||
parentId: targetParentId,
|
||||
})
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.folderUploaded'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.uploadError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, createFile, createFolder, onClose, parentId, t])
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
if (treeRef?.current) {
|
||||
const targetNode = treeRef.current.get(nodeId)
|
||||
targetNode?.edit()
|
||||
}
|
||||
else if (node) {
|
||||
node.edit()
|
||||
}
|
||||
onClose()
|
||||
}, [nodeId, node, onClose, treeRef])
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
setShowDeleteConfirm(true)
|
||||
}, [])
|
||||
|
||||
const handleDeleteConfirm = useCallback(async () => {
|
||||
const isFolder = node?.data?.node_type === 'folder'
|
||||
try {
|
||||
const descendantFileIds = treeData?.children
|
||||
? getAllDescendantFileIds(nodeId, treeData.children)
|
||||
: []
|
||||
|
||||
await deleteNode.mutateAsync({ appId, nodeId })
|
||||
|
||||
descendantFileIds.forEach((fileId) => {
|
||||
storeApi.getState().closeTab(fileId)
|
||||
storeApi.getState().clearDraftContent(fileId)
|
||||
})
|
||||
|
||||
// Also close and clear the node itself if it's a file
|
||||
if (!isFolder) {
|
||||
storeApi.getState().closeTab(nodeId)
|
||||
storeApi.getState().clearDraftContent(nodeId)
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: isFolder
|
||||
? t('skillSidebar.menu.deleted')
|
||||
: t('skillSidebar.menu.fileDeleted'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: isFolder
|
||||
? t('skillSidebar.menu.deleteError')
|
||||
: t('skillSidebar.menu.fileDeleteError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setShowDeleteConfirm(false)
|
||||
onClose()
|
||||
}
|
||||
}, [appId, nodeId, node?.data?.node_type, deleteNode, storeApi, treeData?.children, onClose, t])
|
||||
|
||||
const handleDeleteCancel = useCallback(() => {
|
||||
setShowDeleteConfirm(false)
|
||||
}, [])
|
||||
|
||||
const isLoading = createFile.isPending || createFolder.isPending || deleteNode.isPending
|
||||
const modifyOps = useModifyOperations({
|
||||
nodeId,
|
||||
node,
|
||||
treeRef,
|
||||
appId,
|
||||
storeApi,
|
||||
treeData,
|
||||
onClose,
|
||||
})
|
||||
|
||||
return {
|
||||
fileInputRef,
|
||||
folderInputRef,
|
||||
showDeleteConfirm,
|
||||
isLoading,
|
||||
isDeleting: deleteNode.isPending,
|
||||
handleNewFile,
|
||||
handleNewFolder,
|
||||
handleFileChange,
|
||||
handleFolderChange,
|
||||
handleRename,
|
||||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
handleDeleteCancel,
|
||||
// Create operations
|
||||
fileInputRef: createOps.fileInputRef,
|
||||
folderInputRef: createOps.folderInputRef,
|
||||
handleNewFile: createOps.handleNewFile,
|
||||
handleNewFolder: createOps.handleNewFolder,
|
||||
handleFileChange: createOps.handleFileChange,
|
||||
handleFolderChange: createOps.handleFolderChange,
|
||||
|
||||
// Modify operations
|
||||
showDeleteConfirm: modifyOps.showDeleteConfirm,
|
||||
handleRename: modifyOps.handleRename,
|
||||
handleDeleteClick: modifyOps.handleDeleteClick,
|
||||
handleDeleteConfirm: modifyOps.handleDeleteConfirm,
|
||||
handleDeleteCancel: modifyOps.handleDeleteCancel,
|
||||
|
||||
// Combined loading states
|
||||
isLoading: createOps.isCreating || modifyOps.isDeleting,
|
||||
isDeleting: modifyOps.isDeleting,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use client'
|
||||
|
||||
// Folder node file drop handler with VSCode-style blink animation and auto-expand
|
||||
|
||||
import type { NodeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
107
web/app/components/workflow/skill/hooks/use-modify-operations.ts
Normal file
107
web/app/components/workflow/skill/hooks/use-modify-operations.ts
Normal file
@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
|
||||
// Handles file/folder rename and delete operations
|
||||
|
||||
import type { NodeApi, TreeApi } from 'react-arborist'
|
||||
import type { StoreApi } from 'zustand'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import type { SkillEditorSliceShape } from '@/app/components/workflow/store/workflow/skill-editor/types'
|
||||
import type { AppAssetTreeResponse } from '@/types/app-asset'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useDeleteAppAssetNode } from '@/service/use-app-asset'
|
||||
import { getAllDescendantFileIds } from '../utils/tree-utils'
|
||||
|
||||
type UseModifyOperationsOptions = {
|
||||
nodeId: string
|
||||
node?: NodeApi<TreeNodeData>
|
||||
treeRef?: React.RefObject<TreeApi<TreeNodeData> | null>
|
||||
appId: string
|
||||
storeApi: StoreApi<SkillEditorSliceShape>
|
||||
treeData?: AppAssetTreeResponse
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function useModifyOperations({
|
||||
nodeId,
|
||||
node,
|
||||
treeRef,
|
||||
appId,
|
||||
storeApi,
|
||||
treeData,
|
||||
onClose,
|
||||
}: UseModifyOperationsOptions) {
|
||||
const { t } = useTranslation('workflow')
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const deleteNode = useDeleteAppAssetNode()
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
if (treeRef?.current) {
|
||||
const targetNode = treeRef.current.get(nodeId)
|
||||
targetNode?.edit()
|
||||
}
|
||||
else if (node) {
|
||||
node.edit()
|
||||
}
|
||||
onClose()
|
||||
}, [nodeId, node, onClose, treeRef])
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
setShowDeleteConfirm(true)
|
||||
}, [])
|
||||
|
||||
const handleDeleteConfirm = useCallback(async () => {
|
||||
const isFolder = node?.data?.node_type === 'folder'
|
||||
try {
|
||||
const descendantFileIds = treeData?.children
|
||||
? getAllDescendantFileIds(nodeId, treeData.children)
|
||||
: []
|
||||
|
||||
await deleteNode.mutateAsync({ appId, nodeId })
|
||||
|
||||
descendantFileIds.forEach((fileId) => {
|
||||
storeApi.getState().closeTab(fileId)
|
||||
storeApi.getState().clearDraftContent(fileId)
|
||||
})
|
||||
|
||||
// Also close and clear the node itself if it's a file
|
||||
if (!isFolder) {
|
||||
storeApi.getState().closeTab(nodeId)
|
||||
storeApi.getState().clearDraftContent(nodeId)
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: isFolder
|
||||
? t('skillSidebar.menu.deleted')
|
||||
: t('skillSidebar.menu.fileDeleted'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: isFolder
|
||||
? t('skillSidebar.menu.deleteError')
|
||||
: t('skillSidebar.menu.fileDeleteError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setShowDeleteConfirm(false)
|
||||
onClose()
|
||||
}
|
||||
}, [appId, nodeId, node?.data?.node_type, deleteNode, storeApi, treeData?.children, onClose, t])
|
||||
|
||||
const handleDeleteCancel = useCallback(() => {
|
||||
setShowDeleteConfirm(false)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
showDeleteConfirm,
|
||||
isDeleting: deleteNode.isPending,
|
||||
handleRename,
|
||||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
handleDeleteCancel,
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
'use client'
|
||||
|
||||
// Root-level file drop handler with drag counter to handle nested DOM events
|
||||
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { isFileDrag } from '../utils/drag-utils'
|
||||
import { useFileDrop } from './use-file-drop'
|
||||
|
||||
Reference in New Issue
Block a user