mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
refactor(skill): regroup skill body, file tree, and tree hooks
This commit is contained in:
@ -0,0 +1,179 @@
|
||||
'use client'
|
||||
|
||||
import type { StoreApi } from 'zustand'
|
||||
import type { SkillEditorSliceShape } from '@/app/components/workflow/store/workflow/skill-editor/types'
|
||||
import type { BatchUploadNodeInput } from '@/types/app-asset'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import {
|
||||
useBatchUpload,
|
||||
useCreateAppAssetFolder,
|
||||
useUploadFileWithPresignedUrl,
|
||||
} from '@/service/use-app-asset'
|
||||
import { prepareSkillUploadFile } from '../../../utils/skill-upload-utils'
|
||||
import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration'
|
||||
|
||||
type UseCreateOperationsOptions = {
|
||||
parentId: string | null
|
||||
appId: string
|
||||
storeApi: StoreApi<SkillEditorSliceShape>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const getRelativePath = (file: File) => {
|
||||
return (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name
|
||||
}
|
||||
|
||||
export function useCreateOperations({
|
||||
parentId,
|
||||
appId,
|
||||
storeApi,
|
||||
onClose,
|
||||
}: UseCreateOperationsOptions) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const folderInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const createFolder = useCreateAppAssetFolder()
|
||||
const uploadFile = useUploadFileWithPresignedUrl()
|
||||
const batchUpload = useBatchUpload()
|
||||
const emitTreeUpdate = useSkillTreeUpdateEmitter()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const total = files.length
|
||||
const progress = { uploaded: 0, failed: 0 }
|
||||
|
||||
storeApi.getState().setUploadStatus('uploading')
|
||||
storeApi.getState().setUploadProgress({ uploaded: 0, total, failed: 0 })
|
||||
|
||||
try {
|
||||
const uploadFiles = await Promise.all(files.map(file => prepareSkillUploadFile(file)))
|
||||
await Promise.all(
|
||||
uploadFiles.map(async (file) => {
|
||||
try {
|
||||
await uploadFile.mutateAsync({ appId, file, parentId })
|
||||
progress.uploaded++
|
||||
}
|
||||
catch {
|
||||
progress.failed++
|
||||
}
|
||||
storeApi.getState().setUploadProgress({ uploaded: progress.uploaded, total, failed: progress.failed })
|
||||
}),
|
||||
)
|
||||
|
||||
storeApi.getState().setUploadStatus(progress.failed > 0 ? 'partial_error' : 'success')
|
||||
storeApi.getState().setUploadProgress({ uploaded: progress.uploaded, total, failed: progress.failed })
|
||||
}
|
||||
catch {
|
||||
storeApi.getState().setUploadStatus('partial_error')
|
||||
}
|
||||
finally {
|
||||
if (progress.uploaded > 0)
|
||||
emitTreeUpdate()
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, uploadFile, onClose, parentId, storeApi, emitTreeUpdate])
|
||||
|
||||
const handleFolderChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length === 0) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
|
||||
storeApi.getState().setUploadStatus('uploading')
|
||||
storeApi.getState().setUploadProgress({ uploaded: 0, total: files.length, failed: 0 })
|
||||
|
||||
try {
|
||||
const fileMap = new Map<string, File>()
|
||||
const tree: BatchUploadNodeInput[] = []
|
||||
const folderMap = new Map<string, BatchUploadNodeInput>()
|
||||
const uploadFiles = await Promise.all(files.map(async (file) => {
|
||||
const relativePath = getRelativePath(file)
|
||||
const uploadFile = await prepareSkillUploadFile(file)
|
||||
return { relativePath, uploadFile }
|
||||
}))
|
||||
|
||||
for (const { relativePath, uploadFile } of uploadFiles) {
|
||||
fileMap.set(relativePath, uploadFile)
|
||||
|
||||
const parts = relativePath.split('/')
|
||||
let currentLevel = tree
|
||||
let currentPath = ''
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
const isLastPart = i === parts.length - 1
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part
|
||||
|
||||
if (isLastPart) {
|
||||
currentLevel.push({
|
||||
name: part,
|
||||
node_type: 'file',
|
||||
size: uploadFile.size,
|
||||
})
|
||||
}
|
||||
else {
|
||||
let folder = folderMap.get(currentPath)
|
||||
if (!folder) {
|
||||
folder = {
|
||||
name: part,
|
||||
node_type: 'folder',
|
||||
children: [],
|
||||
}
|
||||
folderMap.set(currentPath, folder)
|
||||
currentLevel.push(folder)
|
||||
}
|
||||
currentLevel = folder.children!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await batchUpload.mutateAsync({
|
||||
appId,
|
||||
tree,
|
||||
files: fileMap,
|
||||
parentId,
|
||||
onProgress: (uploaded, total) => {
|
||||
storeApi.getState().setUploadProgress({ uploaded, total, failed: 0 })
|
||||
},
|
||||
})
|
||||
|
||||
storeApi.getState().setUploadStatus('success')
|
||||
storeApi.getState().setUploadProgress({ uploaded: files.length, total: files.length, failed: 0 })
|
||||
emitTreeUpdate()
|
||||
}
|
||||
catch {
|
||||
storeApi.getState().setUploadStatus('partial_error')
|
||||
}
|
||||
finally {
|
||||
e.target.value = ''
|
||||
onClose()
|
||||
}
|
||||
}, [appId, batchUpload, onClose, parentId, storeApi, emitTreeUpdate])
|
||||
|
||||
return {
|
||||
fileInputRef,
|
||||
folderInputRef,
|
||||
isCreating: uploadFile.isPending || createFolder.isPending || batchUpload.isPending,
|
||||
handleNewFile,
|
||||
handleNewFolder,
|
||||
handleFileChange,
|
||||
handleFolderChange,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { consoleClient } from '@/service/client'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
|
||||
type UseDownloadOperationOptions = {
|
||||
appId: string
|
||||
nodeId: string
|
||||
fileName?: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function useDownloadOperation({
|
||||
appId,
|
||||
nodeId,
|
||||
fileName,
|
||||
onClose,
|
||||
}: UseDownloadOperationOptions) {
|
||||
const { t } = useTranslation('workflow')
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
if (!nodeId || !appId)
|
||||
return
|
||||
|
||||
onClose()
|
||||
|
||||
setIsDownloading(true)
|
||||
try {
|
||||
const { download_url } = await consoleClient.appAsset.getFileDownloadUrl({
|
||||
params: { appId, nodeId },
|
||||
})
|
||||
|
||||
downloadUrl({ url: download_url, fileName })
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.downloadError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setIsDownloading(false)
|
||||
}
|
||||
}, [appId, nodeId, fileName, onClose, t])
|
||||
|
||||
return {
|
||||
handleDownload,
|
||||
isDownloading,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
'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 { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { toApiParentId } from '../../../utils/tree-utils'
|
||||
import { useSkillAssetTreeData } from '../data/use-skill-asset-tree'
|
||||
import { useCreateOperations } from './use-create-operations'
|
||||
import { useDownloadOperation } from './use-download-operation'
|
||||
import { useModifyOperations } from './use-modify-operations'
|
||||
|
||||
type UseFileOperationsOptions = {
|
||||
nodeId?: string
|
||||
onClose: () => void
|
||||
treeRef?: React.RefObject<TreeApi<TreeNodeData> | null>
|
||||
node?: NodeApi<TreeNodeData>
|
||||
}
|
||||
|
||||
export function useFileOperations({
|
||||
nodeId: explicitNodeId,
|
||||
onClose,
|
||||
treeRef,
|
||||
node,
|
||||
}: UseFileOperationsOptions) {
|
||||
const nodeId = node?.data.id ?? explicitNodeId ?? ''
|
||||
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const storeApi = useWorkflowStore()
|
||||
const { data: treeData } = useSkillAssetTreeData()
|
||||
|
||||
const parentId = toApiParentId(nodeId)
|
||||
|
||||
const createOps = useCreateOperations({
|
||||
parentId,
|
||||
appId,
|
||||
storeApi,
|
||||
onClose,
|
||||
})
|
||||
|
||||
const modifyOps = useModifyOperations({
|
||||
nodeId,
|
||||
node,
|
||||
treeRef,
|
||||
appId,
|
||||
storeApi,
|
||||
treeData,
|
||||
onClose,
|
||||
})
|
||||
|
||||
const downloadOps = useDownloadOperation({
|
||||
appId,
|
||||
nodeId,
|
||||
fileName: node?.data.name,
|
||||
onClose,
|
||||
})
|
||||
|
||||
return {
|
||||
// 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,
|
||||
|
||||
// Download operation
|
||||
handleDownload: downloadOps.handleDownload,
|
||||
|
||||
// Combined loading states
|
||||
isLoading: createOps.isCreating || modifyOps.isDeleting || downloadOps.isDownloading,
|
||||
isDeleting: modifyOps.isDeleting,
|
||||
isDownloading: downloadOps.isDownloading,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
'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'
|
||||
import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration'
|
||||
|
||||
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 emitTreeUpdate = useSkillTreeUpdateEmitter()
|
||||
|
||||
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 })
|
||||
emitTreeUpdate()
|
||||
|
||||
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, emitTreeUpdate])
|
||||
|
||||
const handleDeleteCancel = useCallback(() => {
|
||||
setShowDeleteConfirm(false)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
showDeleteConfirm,
|
||||
isDeleting: deleteNode.isPending,
|
||||
handleRename,
|
||||
handleDeleteClick,
|
||||
handleDeleteConfirm,
|
||||
handleDeleteCancel,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
// Internal tree node move handler - API execution logic only
|
||||
// Drag state syncing is handled by react-arborist + TreeNode useEffect
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useMoveAppAssetNode } from '@/service/use-app-asset'
|
||||
import { toApiParentId } from '../../../utils/tree-utils'
|
||||
import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration'
|
||||
|
||||
export function useNodeMove() {
|
||||
const { t } = useTranslation('workflow')
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const moveNode = useMoveAppAssetNode()
|
||||
const emitTreeUpdate = useSkillTreeUpdateEmitter()
|
||||
|
||||
// Execute move API call - validation is handled by react-arborist's disableDrop callback
|
||||
const executeMoveNode = useCallback(async (nodeId: string, targetFolderId: string | null) => {
|
||||
try {
|
||||
await moveNode.mutateAsync({
|
||||
appId,
|
||||
nodeId,
|
||||
payload: { parent_id: toApiParentId(targetFolderId) },
|
||||
})
|
||||
|
||||
emitTreeUpdate()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.moved'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.moveError'),
|
||||
})
|
||||
}
|
||||
}, [appId, moveNode, t, emitTreeUpdate])
|
||||
|
||||
return {
|
||||
executeMoveNode,
|
||||
isMoving: moveNode.isPending,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
|
||||
// Internal tree node reorder handler - API execution logic only
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useReorderAppAssetNode } from '@/service/use-app-asset'
|
||||
import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration'
|
||||
|
||||
export function useNodeReorder() {
|
||||
const { t } = useTranslation('workflow')
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const reorderNode = useReorderAppAssetNode()
|
||||
const emitTreeUpdate = useSkillTreeUpdateEmitter()
|
||||
|
||||
const executeReorderNode = useCallback(async (nodeId: string, afterNodeId: string | null) => {
|
||||
try {
|
||||
await reorderNode.mutateAsync({
|
||||
appId,
|
||||
nodeId,
|
||||
payload: { after_node_id: afterNodeId },
|
||||
})
|
||||
|
||||
emitTreeUpdate()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.moved'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.moveError'),
|
||||
})
|
||||
}
|
||||
}, [appId, reorderNode, t, emitTreeUpdate])
|
||||
|
||||
return {
|
||||
executeReorderNode,
|
||||
isReordering: reorderNode.isPending,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import type { TreeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from '../../../type'
|
||||
import type { AppAssetTreeResponse } from '@/types/app-asset'
|
||||
import { useCallback, useEffect, useRef } 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 { useMoveAppAssetNode } from '@/service/use-app-asset'
|
||||
import { findNodeById, getTargetFolderIdFromSelection, toApiParentId } from '../../../utils/tree-utils'
|
||||
import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration'
|
||||
|
||||
type UsePasteOperationOptions = {
|
||||
treeRef: RefObject<TreeApi<TreeNodeData> | null>
|
||||
treeData?: AppAssetTreeResponse
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
type UsePasteOperationReturn = {
|
||||
isPasting: boolean
|
||||
handlePaste: () => void
|
||||
}
|
||||
|
||||
export function usePasteOperation({
|
||||
treeRef,
|
||||
treeData,
|
||||
enabled = true,
|
||||
}: UsePasteOperationOptions): UsePasteOperationReturn {
|
||||
const { t } = useTranslation('workflow')
|
||||
const storeApi = useWorkflowStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const moveNode = useMoveAppAssetNode()
|
||||
const emitTreeUpdate = useSkillTreeUpdateEmitter()
|
||||
const isPastingRef = useRef(false)
|
||||
|
||||
const handlePaste = useCallback(async () => {
|
||||
if (isPastingRef.current)
|
||||
return
|
||||
|
||||
const clipboard = storeApi.getState().clipboard
|
||||
if (!clipboard || clipboard.nodeIds.size === 0)
|
||||
return
|
||||
|
||||
const { operation, nodeIds } = clipboard
|
||||
const tree = treeRef.current
|
||||
const treeChildren = treeData?.children ?? []
|
||||
|
||||
const selectedId = tree?.selectedNodes[0]?.id ?? storeApi.getState().selectedTreeNodeId
|
||||
const targetFolderId = getTargetFolderIdFromSelection(selectedId, treeChildren)
|
||||
const targetParentId = toApiParentId(targetFolderId)
|
||||
|
||||
if (operation === 'cut') {
|
||||
const nodeIdsArray = [...nodeIds]
|
||||
const isMovingToSelf = nodeIdsArray.some((nodeId) => {
|
||||
const node = findNodeById(treeChildren, nodeId)
|
||||
if (!node)
|
||||
return false
|
||||
if (node.node_type === 'folder' && nodeId === targetFolderId)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
|
||||
if (isMovingToSelf) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.cannotMoveToSelf'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isPastingRef.current = true
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
nodeIdsArray.map(nodeId =>
|
||||
moveNode.mutateAsync({
|
||||
appId,
|
||||
nodeId,
|
||||
payload: { parent_id: targetParentId },
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
storeApi.getState().clearClipboard()
|
||||
emitTreeUpdate()
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.moved'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.moveError'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
isPastingRef.current = false
|
||||
}
|
||||
}
|
||||
}, [appId, moveNode, storeApi, t, treeData?.children, treeRef, emitTreeUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled)
|
||||
return
|
||||
|
||||
const handlePasteEvent = () => {
|
||||
handlePaste()
|
||||
}
|
||||
|
||||
window.addEventListener('skill:paste', handlePasteEvent)
|
||||
return () => {
|
||||
window.removeEventListener('skill:paste', handlePasteEvent)
|
||||
}
|
||||
}, [enabled, handlePaste])
|
||||
|
||||
return {
|
||||
isPasting: moveNode.isPending,
|
||||
handlePaste,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user