mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 17:38:04 +08:00
Align skill tree menu behaviors
This commit is contained in:
@ -18,6 +18,8 @@ type UseFileOperationsOptions = {
|
||||
onClose: () => void
|
||||
treeRef?: React.RefObject<TreeApi<TreeNodeData> | null>
|
||||
node?: NodeApi<TreeNodeData>
|
||||
nodeType?: TreeNodeData['node_type']
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
export function useFileOperations({
|
||||
@ -25,8 +27,12 @@ export function useFileOperations({
|
||||
onClose,
|
||||
treeRef,
|
||||
node,
|
||||
nodeType: explicitNodeType,
|
||||
fileName: explicitFileName,
|
||||
}: UseFileOperationsOptions) {
|
||||
const nodeId = node?.data.id ?? explicitNodeId ?? ''
|
||||
const nodeType = node?.data.node_type ?? explicitNodeType
|
||||
const fileName = node?.data.name ?? explicitFileName
|
||||
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
@ -46,6 +52,7 @@ export function useFileOperations({
|
||||
nodeId,
|
||||
node,
|
||||
treeRef,
|
||||
nodeType,
|
||||
appId,
|
||||
storeApi,
|
||||
treeData,
|
||||
@ -55,7 +62,7 @@ export function useFileOperations({
|
||||
const downloadOps = useDownloadOperation({
|
||||
appId,
|
||||
nodeId,
|
||||
fileName: node?.data.name,
|
||||
fileName,
|
||||
onClose,
|
||||
})
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ type UseModifyOperationsOptions = {
|
||||
nodeId: string
|
||||
node?: NodeApi<TreeNodeData>
|
||||
treeRef?: React.RefObject<TreeApi<TreeNodeData> | null>
|
||||
nodeType?: TreeNodeData['node_type']
|
||||
appId: string
|
||||
storeApi: StoreApi<SkillEditorSliceShape>
|
||||
treeData?: AppAssetTreeResponse
|
||||
@ -28,6 +29,7 @@ export function useModifyOperations({
|
||||
nodeId,
|
||||
node,
|
||||
treeRef,
|
||||
nodeType,
|
||||
appId,
|
||||
storeApi,
|
||||
treeData,
|
||||
@ -54,7 +56,7 @@ export function useModifyOperations({
|
||||
}, [])
|
||||
|
||||
const handleDeleteConfirm = useCallback(async () => {
|
||||
const isFolder = node?.data?.node_type === 'folder'
|
||||
const isFolder = (node?.data?.node_type ?? nodeType) === 'folder'
|
||||
try {
|
||||
const descendantFileIds = treeData?.children
|
||||
? getAllDescendantFileIds(nodeId, treeData.children)
|
||||
@ -91,7 +93,7 @@ export function useModifyOperations({
|
||||
setShowDeleteConfirm(false)
|
||||
onClose()
|
||||
}
|
||||
}, [appId, nodeId, node?.data?.node_type, deleteNodeAsync, storeApi, treeData?.children, onClose, t, emitTreeUpdate])
|
||||
}, [appId, nodeId, node?.data?.node_type, nodeType, deleteNodeAsync, storeApi, treeData?.children, onClose, t, emitTreeUpdate])
|
||||
|
||||
const handleDeleteCancel = useCallback(() => {
|
||||
setShowDeleteConfirm(false)
|
||||
|
||||
@ -61,6 +61,7 @@ const mocks = vi.hoisted(() => ({
|
||||
getTargetFolderIdFromSelection: vi.fn<(selectedId: string | null, nodes: TreeNodeData[]) => string>(),
|
||||
toApiParentId: vi.fn<(folderId: string | null | undefined) => string | null>(),
|
||||
findNodeById: vi.fn<(nodes: TreeNodeData[], nodeId: string) => TreeNodeData | null>(),
|
||||
isDescendantOf: vi.fn<(potentialDescendantId: string | null | undefined, ancestorId: string | null | undefined, nodes: TreeNodeData[]) => boolean>(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
@ -88,6 +89,7 @@ vi.mock('../../../utils/tree-utils', () => ({
|
||||
getTargetFolderIdFromSelection: mocks.getTargetFolderIdFromSelection,
|
||||
toApiParentId: mocks.toApiParentId,
|
||||
findNodeById: mocks.findNodeById,
|
||||
isDescendantOf: mocks.isDescendantOf,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
@ -127,6 +129,7 @@ describe('usePasteOperation', () => {
|
||||
mocks.getTargetFolderIdFromSelection.mockReturnValue('target-folder')
|
||||
mocks.toApiParentId.mockReturnValue('target-parent')
|
||||
mocks.findNodeById.mockReturnValue(null)
|
||||
mocks.isDescendantOf.mockReturnValue(false)
|
||||
})
|
||||
|
||||
// Scenario: isPasting output should reflect mutation pending state.
|
||||
@ -193,6 +196,30 @@ describe('usePasteOperation', () => {
|
||||
expect(mocks.moveMutateAsync).not.toHaveBeenCalled()
|
||||
expect(mocks.toastError).toHaveBeenCalledWith('workflow.skillSidebar.menu.cannotMoveToSelf')
|
||||
})
|
||||
|
||||
it('should reject moving folder into its descendant and show error toast', async () => {
|
||||
mocks.workflowState.clipboard = {
|
||||
operation: 'cut',
|
||||
nodeIds: new Set(['folder-1']),
|
||||
}
|
||||
mocks.getTargetFolderIdFromSelection.mockReturnValueOnce('child-folder')
|
||||
mocks.findNodeById
|
||||
.mockReturnValueOnce(createTreeNode('folder-1', 'folder'))
|
||||
.mockReturnValueOnce(createTreeNode('folder-1', 'folder'))
|
||||
mocks.isDescendantOf.mockReturnValueOnce(true)
|
||||
const treeRef = createTreeRef('child-folder')
|
||||
const treeData: AppAssetTreeResponse = {
|
||||
children: [createTreeNode('folder-1', 'folder')],
|
||||
}
|
||||
const { result } = renderHook(() => usePasteOperation({ treeRef, treeData }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handlePaste()
|
||||
})
|
||||
|
||||
expect(mocks.moveMutateAsync).not.toHaveBeenCalled()
|
||||
expect(mocks.toastError).toHaveBeenCalledWith('workflow.skillSidebar.menu.cannotMoveToDescendant')
|
||||
})
|
||||
})
|
||||
|
||||
// Scenario: successful cut-paste should move all nodes and clear clipboard.
|
||||
|
||||
@ -10,7 +10,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useMoveAppAssetNode } from '@/service/use-app-asset'
|
||||
import { findNodeById, getTargetFolderIdFromSelection, toApiParentId } from '../../../utils/tree-utils'
|
||||
import { findNodeById, getTargetFolderIdFromSelection, isDescendantOf, toApiParentId } from '../../../utils/tree-utils'
|
||||
import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration'
|
||||
|
||||
type UsePasteOperationOptions = {
|
||||
@ -64,11 +64,23 @@ export function usePasteOperation({
|
||||
return false
|
||||
})
|
||||
|
||||
const isMovingToDescendant = nodeIdsArray.some((nodeId) => {
|
||||
const node = findNodeById(treeChildren, nodeId)
|
||||
if (!node || node.node_type !== 'folder')
|
||||
return false
|
||||
return isDescendantOf(targetFolderId, nodeId, treeChildren)
|
||||
})
|
||||
|
||||
if (isMovingToSelf) {
|
||||
toast.error(t('skillSidebar.menu.cannotMoveToSelf'))
|
||||
return
|
||||
}
|
||||
|
||||
if (isMovingToDescendant) {
|
||||
toast.error(t('skillSidebar.menu.cannotMoveToDescendant'))
|
||||
return
|
||||
}
|
||||
|
||||
isPastingRef.current = true
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user