diff --git a/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx b/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx index 7d1c419d45..80de905352 100644 --- a/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx +++ b/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx @@ -14,7 +14,6 @@ import { NODE_MENU_TYPE } from '../../constants' import NodeMenu from './node-menu' type MockWorkflowState = { - selectedNodeIds: Set hasClipboard: () => boolean } @@ -39,6 +38,7 @@ type RenderNodeMenuProps = { type?: 'root' | 'folder' | 'file' menuType?: 'dropdown' | 'context' nodeId?: string + actionNodeIds?: string[] onClose?: () => void onImportSkills?: () => void } @@ -64,10 +64,10 @@ function createFileOperationsMock(): MockFileOperations { const mocks = vi.hoisted(() => ({ storeState: { - selectedNodeIds: new Set(), hasClipboard: () => false, } as MockWorkflowState, cutNodes: vi.fn(), + setFileTreeSearchTerm: vi.fn(), fileOperations: createFileOperationsMock(), })) @@ -76,6 +76,7 @@ vi.mock('@/app/components/workflow/store', () => ({ useWorkflowStore: () => ({ getState: () => ({ cutNodes: mocks.cutNodes, + setFileTreeSearchTerm: mocks.setFileTreeSearchTerm, }), }), })) @@ -84,6 +85,7 @@ const renderNodeMenu = ({ type = NODE_MENU_TYPE.FOLDER, menuType = 'dropdown', nodeId = 'node-1', + actionNodeIds, onClose = vi.fn(), onImportSkills, }: RenderNodeMenuProps = {}) => { @@ -96,6 +98,7 @@ const renderNodeMenu = ({ type={type} menuType={menuType} nodeId={nodeId} + actionNodeIds={actionNodeIds} onClose={onClose} fileInputRef={mocks.fileOperations.fileInputRef} folderInputRef={mocks.fileOperations.folderInputRef} @@ -120,6 +123,7 @@ const renderNodeMenu = ({ type={type} menuType={menuType} nodeId={nodeId} + actionNodeIds={actionNodeIds} onClose={onClose} fileInputRef={mocks.fileOperations.fileInputRef} folderInputRef={mocks.fileOperations.folderInputRef} @@ -147,7 +151,6 @@ const renderNodeMenu = ({ describe('NodeMenu', () => { beforeEach(() => { vi.clearAllMocks() - mocks.storeState.selectedNodeIds = new Set() mocks.storeState.hasClipboard = () => false mocks.fileOperations = createFileOperationsMock() }) @@ -195,6 +198,8 @@ describe('NodeMenu', () => { fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFile/i })) fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFolder/i })) + expect(mocks.setFileTreeSearchTerm).toHaveBeenNthCalledWith(1, '') + expect(mocks.setFileTreeSearchTerm).toHaveBeenNthCalledWith(2, '') expect(mocks.fileOperations.handleNewFile).toHaveBeenCalledTimes(1) expect(mocks.fileOperations.handleNewFolder).toHaveBeenCalledTimes(1) }) @@ -209,9 +214,12 @@ describe('NodeMenu', () => { expect(clickSpy).toHaveBeenCalledTimes(2) }) - it('should cut selected nodes and close menu when cut is clicked', () => { - mocks.storeState.selectedNodeIds = new Set(['file-1', 'file-2']) - const { onClose } = renderNodeMenu({ type: NODE_MENU_TYPE.FILE, nodeId: 'fallback-id' }) + it('should cut explicit action node ids and close menu when cut is clicked', () => { + const { onClose } = renderNodeMenu({ + type: NODE_MENU_TYPE.FILE, + nodeId: 'fallback-id', + actionNodeIds: ['file-1', 'file-2'], + }) fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.cut/i })) diff --git a/web/app/components/workflow/skill/file-tree/tree/node-menu.tsx b/web/app/components/workflow/skill/file-tree/tree/node-menu.tsx index c6ae521466..58295ef10d 100644 --- a/web/app/components/workflow/skill/file-tree/tree/node-menu.tsx +++ b/web/app/components/workflow/skill/file-tree/tree/node-menu.tsx @@ -24,6 +24,7 @@ type NodeMenuProps = { type: NodeMenuType menuType: 'dropdown' | 'context' nodeId?: string + actionNodeIds?: string[] onClose: () => void fileInputRef: React.RefObject folderInputRef: React.RefObject @@ -42,6 +43,7 @@ const NodeMenu = ({ type, menuType, nodeId, + actionNodeIds, onClose, fileInputRef, folderInputRef, @@ -57,7 +59,6 @@ const NodeMenu = ({ }: NodeMenuProps) => { const { t } = useTranslation('workflow') const storeApi = useWorkflowStore() - const selectedNodeIds = useStore(s => s.selectedNodeIds) const hasClipboard = useStore(s => s.hasClipboard()) const isRoot = type === NODE_MENU_TYPE.ROOT const isFolder = type === NODE_MENU_TYPE.FOLDER || isRoot @@ -65,18 +66,30 @@ const NodeMenu = ({ const currentNodeId = nodeId const handleCut = useCallback(() => { - const ids = selectedNodeIds.size > 0 ? [...selectedNodeIds] : (currentNodeId ? [currentNodeId] : []) + const ids = actionNodeIds && actionNodeIds.length > 0 + ? actionNodeIds + : (currentNodeId ? [currentNodeId] : []) if (ids.length > 0) { storeApi.getState().cutNodes(ids) onClose() } - }, [currentNodeId, onClose, selectedNodeIds, storeApi]) + }, [actionNodeIds, currentNodeId, onClose, storeApi]) const handlePaste = useCallback(() => { window.dispatchEvent(new CustomEvent('skill:paste')) onClose() }, [onClose]) + const handleCreateFile = useCallback(() => { + storeApi.getState().setFileTreeSearchTerm('') + onNewFile() + }, [onNewFile, storeApi]) + + const handleCreateFolder = useCallback(() => { + storeApi.getState().setFileTreeSearchTerm('') + onNewFolder() + }, [onNewFolder, storeApi]) + const showRenameDelete = isFolder ? !isRoot : true const Separator = menuType === 'dropdown' ? DropdownMenuSeparator : ContextMenuSeparator @@ -106,14 +119,14 @@ const NodeMenu = ({ menuType={menuType} icon={FileAdd} label={t('skillSidebar.menu.newFile')} - onClick={onNewFile} + onClick={handleCreateFile} disabled={isLoading} /> diff --git a/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.spec.tsx b/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.spec.tsx index 241be57ee4..63cf90d3e9 100644 --- a/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.spec.tsx +++ b/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.spec.tsx @@ -3,6 +3,7 @@ import { ROOT_ID } from '../../constants' import TreeContextMenu from './tree-context-menu' const mocks = vi.hoisted(() => ({ + selectedNodeIds: new Set(), clearSelection: vi.fn(), setSelectedNodeIds: vi.fn(), deselectAll: vi.fn(), @@ -30,6 +31,7 @@ const mocks = vi.hoisted(() => ({ vi.mock('@/app/components/workflow/store', () => ({ useWorkflowStore: () => ({ getState: () => ({ + selectedNodeIds: mocks.selectedNodeIds, clearSelection: mocks.clearSelection, setSelectedNodeIds: mocks.setSelectedNodeIds, }), @@ -63,11 +65,12 @@ vi.mock('../../hooks/file-tree/operations/use-file-operations', () => ({ })) vi.mock('./node-menu', () => ({ - default: ({ type, menuType, nodeId, onImportSkills }: { type: string, menuType: string, nodeId?: string, onImportSkills?: () => void }) => ( + default: ({ type, menuType, nodeId, actionNodeIds, onImportSkills }: { type: string, menuType: string, nodeId?: string, actionNodeIds?: string[], onImportSkills?: () => void }) => (
{onImportSkills && (
@@ -179,6 +184,7 @@ describe('TreeNode', () => { workflowState.dirtyContents.clear() workflowState.cutNodeIds.clear() + workflowState.selectedNodeIds = new Set() workflowState.dragOverFolderId = null dndMocks.isDragOver = false @@ -202,6 +208,7 @@ describe('TreeNode', () => { }) it('should render selected open folder with folder expansion aria state', () => { + workflowState.selectedNodeIds = new Set(['folder-1', 'folder-2']) const props = buildProps({ id: 'folder-1', name: 'src', @@ -216,6 +223,8 @@ describe('TreeNode', () => { expect(treeItem).toHaveAttribute('aria-selected', 'true') expect(treeItem).toHaveAttribute('aria-expanded', 'true') expect(treeItem).toHaveClass('bg-state-base-active') + fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.moreActions/i })) + expect(screen.getByTestId('node-menu-dropdown')).toHaveAttribute('data-action-node-ids', 'folder-1,folder-2') }) it('should apply drag-over, blinking, and cut styles when states are active', () => { diff --git a/web/app/components/workflow/skill/file-tree/tree/tree-node.tsx b/web/app/components/workflow/skill/file-tree/tree/tree-node.tsx index 57321855a6..99f2ddfbfd 100644 --- a/web/app/components/workflow/skill/file-tree/tree/tree-node.tsx +++ b/web/app/components/workflow/skill/file-tree/tree/tree-node.tsx @@ -3,7 +3,7 @@ import type { NodeRendererProps } from 'react-arborist' import type { TreeNodeData } from '../../type' import * as React from 'react' -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { DropdownMenu, @@ -32,7 +32,13 @@ const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => { const isSelected = node.isSelected const isDirty = useStore(s => s.dirtyContents.has(node.data.id)) const isCut = useStore(s => s.isCutNode(node.data.id)) + const selectedNodeIds = useStore(s => s.selectedNodeIds) const storeApi = useWorkflowStore() + const actionNodeIds = useMemo(() => { + if (node.isSelected && selectedNodeIds.size > 0) + return [...selectedNodeIds] + return [node.data.id] + }, [node.data.id, node.isSelected, selectedNodeIds]) // Sync react-arborist drag state to Zustand for DragActionTooltip const prevIsDraggingRef = useRef(node.isDragging) @@ -181,6 +187,7 @@ const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => { menuType="dropdown" type={isFolder ? 'folder' : 'file'} nodeId={node.data.id} + actionNodeIds={actionNodeIds} onClose={handleMenuClose} fileInputRef={fileOperations.fileInputRef} folderInputRef={fileOperations.folderInputRef} diff --git a/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.spec.tsx b/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.spec.tsx index 6cdfe18097..d5afb4962b 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.spec.tsx +++ b/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.spec.tsx @@ -19,6 +19,7 @@ const mocks = vi.hoisted(() => ({ toastSuccess: vi.fn<(message: string) => void>(), toastError: vi.fn<(message: string) => void>(), getAllDescendantFileIds: vi.fn<(nodeId: string, nodes: TreeNodeData[]) => string[]>(), + isDescendantOf: vi.fn<(potentialDescendantId: string | null | undefined, ancestorId: string | null | undefined, nodes: TreeNodeData[]) => boolean>(), })) vi.mock('@/service/use-app-asset', () => ({ @@ -34,6 +35,7 @@ vi.mock('../data/use-skill-tree-collaboration', () => ({ vi.mock('../../../utils/tree-utils', () => ({ getAllDescendantFileIds: mocks.getAllDescendantFileIds, + isDescendantOf: mocks.isDescendantOf, })) vi.mock('@/app/components/base/ui/toast', () => ({ @@ -75,19 +77,27 @@ const createTreeRef = (targetNode: NodeApi | null) => { const createStoreApi = () => { const closeTab = vi.fn<(fileId: string) => void>() const clearDraftContent = vi.fn<(fileId: string) => void>() + const clearFileMetadata = vi.fn<(fileId: string) => void>() + const clearClipboard = vi.fn<() => void>() const state = { + clipboard: null, closeTab, clearDraftContent, - } as Pick + clearFileMetadata, + clearClipboard, + } as Pick const storeApi = { getState: () => state, } as unknown as StoreApi return { + state, storeApi, closeTab, clearDraftContent, + clearFileMetadata, + clearClipboard, } } @@ -97,6 +107,7 @@ describe('useModifyOperations', () => { mocks.deletePending = false mocks.deleteMutateAsync.mockResolvedValue(undefined) mocks.getAllDescendantFileIds.mockReturnValue([]) + mocks.isDescendantOf.mockReturnValue(false) }) // Scenario: loading state should match mutation pending status. @@ -197,13 +208,17 @@ describe('useModifyOperations', () => { // Scenario: successful deletes should close tabs/drafts and emit collaboration updates. describe('Delete success', () => { it('should delete file node, clear descendants and current file tabs, and show file success toast', async () => { - const { storeApi, closeTab, clearDraftContent } = createStoreApi() + const { storeApi, closeTab, clearDraftContent, clearFileMetadata, clearClipboard, state } = createStoreApi() const onClose = vi.fn() const { node } = createNodeApi('file', 'file-7') const treeData: AppAssetTreeResponse = { children: [createTreeNodeData('root-folder', 'folder')], } mocks.getAllDescendantFileIds.mockReturnValue(['desc-1', 'desc-2']) + state.clipboard = { + operation: 'cut', + nodeIds: new Set(['file-7']), + } const { result } = renderHook(() => useModifyOperations({ nodeId: 'file-7', @@ -235,6 +250,10 @@ describe('useModifyOperations', () => { expect(clearDraftContent).toHaveBeenNthCalledWith(1, 'desc-1') expect(clearDraftContent).toHaveBeenNthCalledWith(2, 'desc-2') expect(clearDraftContent).toHaveBeenNthCalledWith(3, 'file-7') + expect(clearFileMetadata).toHaveBeenNthCalledWith(1, 'desc-1') + expect(clearFileMetadata).toHaveBeenNthCalledWith(2, 'desc-2') + expect(clearFileMetadata).toHaveBeenNthCalledWith(3, 'file-7') + expect(clearClipboard).toHaveBeenCalledTimes(1) expect(mocks.toastSuccess).toHaveBeenCalledWith('workflow.skillSidebar.menu.fileDeleted') expect(result.current.showDeleteConfirm).toBe(false) @@ -242,12 +261,17 @@ describe('useModifyOperations', () => { }) it('should delete folder node and skip closing the folder tab itself', async () => { - const { storeApi, closeTab, clearDraftContent } = createStoreApi() + const { storeApi, closeTab, clearDraftContent, clearFileMetadata, clearClipboard, state } = createStoreApi() const { node } = createNodeApi('folder', 'folder-9') const treeData: AppAssetTreeResponse = { children: [createTreeNodeData('root-folder', 'folder')], } mocks.getAllDescendantFileIds.mockReturnValue(['file-in-folder']) + mocks.isDescendantOf.mockReturnValueOnce(true) + state.clipboard = { + operation: 'cut', + nodeIds: new Set(['nested-folder']), + } const { result } = renderHook(() => useModifyOperations({ nodeId: 'folder-9', @@ -266,6 +290,9 @@ describe('useModifyOperations', () => { expect(closeTab).toHaveBeenCalledWith('file-in-folder') expect(clearDraftContent).toHaveBeenCalledTimes(1) expect(clearDraftContent).toHaveBeenCalledWith('file-in-folder') + expect(clearFileMetadata).toHaveBeenCalledTimes(1) + expect(clearFileMetadata).toHaveBeenCalledWith('file-in-folder') + expect(clearClipboard).toHaveBeenCalledTimes(1) expect(closeTab).not.toHaveBeenCalledWith('folder-9') expect(clearDraftContent).not.toHaveBeenCalledWith('folder-9') expect(mocks.toastSuccess).toHaveBeenCalledWith('workflow.skillSidebar.menu.deleted') @@ -276,7 +303,7 @@ describe('useModifyOperations', () => { describe('Delete errors', () => { it('should show folder delete error toast on failure', async () => { mocks.deleteMutateAsync.mockRejectedValueOnce(new Error('delete failed')) - const { storeApi, closeTab, clearDraftContent } = createStoreApi() + const { storeApi, closeTab, clearDraftContent, clearFileMetadata, clearClipboard } = createStoreApi() const onClose = vi.fn() const { node } = createNodeApi('folder', 'folder-err') const treeData: AppAssetTreeResponse = { @@ -299,6 +326,8 @@ describe('useModifyOperations', () => { expect(mocks.emitTreeUpdate).not.toHaveBeenCalled() expect(closeTab).not.toHaveBeenCalled() expect(clearDraftContent).not.toHaveBeenCalled() + expect(clearFileMetadata).not.toHaveBeenCalled() + expect(clearClipboard).not.toHaveBeenCalled() expect(mocks.toastError).toHaveBeenCalledWith('workflow.skillSidebar.menu.deleteError') expect(result.current.showDeleteConfirm).toBe(false) expect(onClose).toHaveBeenCalledTimes(1) diff --git a/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.ts b/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.ts index ad07a18c3d..0afe042aa2 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.ts +++ b/web/app/components/workflow/skill/hooks/file-tree/operations/use-modify-operations.ts @@ -11,7 +11,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from '@/app/components/base/ui/toast' import { useDeleteAppAssetNode } from '@/service/use-app-asset' -import { getAllDescendantFileIds } from '../../../utils/tree-utils' +import { getAllDescendantFileIds, isDescendantOf } from '../../../utils/tree-utils' import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration' type UseModifyOperationsOptions = { @@ -61,19 +61,31 @@ export function useModifyOperations({ const descendantFileIds = treeData?.children ? getAllDescendantFileIds(nodeId, treeData.children) : [] + const affectedFileIds = Array.from(new Set( + isFolder ? descendantFileIds : [...descendantFileIds, nodeId], + )) await deleteNodeAsync({ appId, nodeId }) emitTreeUpdate() - descendantFileIds.forEach((fileId) => { + affectedFileIds.forEach((fileId) => { storeApi.getState().closeTab(fileId) storeApi.getState().clearDraftContent(fileId) + storeApi.getState().clearFileMetadata(fileId) }) - // Also close and clear the node itself if it's a file - if (!isFolder) { - storeApi.getState().closeTab(nodeId) - storeApi.getState().clearDraftContent(nodeId) + const clipboard = storeApi.getState().clipboard + if (clipboard) { + const shouldClearClipboard = [...clipboard.nodeIds].some((clipboardNodeId) => { + if (clipboardNodeId === nodeId) + return true + if (!isFolder || !treeData?.children) + return false + return isDescendantOf(clipboardNodeId, nodeId, treeData.children) + }) + + if (shouldClearClipboard) + storeApi.getState().clearClipboard() } toast.success( diff --git a/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.spec.tsx b/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.spec.tsx index e2f2f8f33f..a4c48b1269 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.spec.tsx +++ b/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.spec.tsx @@ -35,6 +35,7 @@ type WorkflowStoreState = { nodeIds: Set } | null selectedTreeNodeId: string | null + cutNodes: (nodeIds: string[]) => void clearClipboard: () => void } @@ -48,6 +49,7 @@ const mocks = vi.hoisted(() => ({ workflowState: { clipboard: null, selectedTreeNodeId: null, + cutNodes: vi.fn<(nodeIds: string[]) => void>(), clearClipboard: vi.fn<() => void>(), } as WorkflowStoreState, appStoreState: { @@ -123,6 +125,7 @@ describe('usePasteOperation', () => { vi.clearAllMocks() mocks.workflowState.clipboard = null mocks.workflowState.selectedTreeNodeId = null + mocks.workflowState.cutNodes = vi.fn() mocks.appStoreState.appDetail = { id: 'app-1' } mocks.movePending = false mocks.moveMutateAsync.mockResolvedValue(undefined) @@ -303,10 +306,37 @@ describe('usePasteOperation', () => { }) expect(mocks.workflowState.clearClipboard).not.toHaveBeenCalled() + expect(mocks.workflowState.cutNodes).not.toHaveBeenCalled() expect(mocks.emitTreeUpdate).not.toHaveBeenCalled() expect(mocks.toastError).toHaveBeenCalledWith('workflow.skillSidebar.menu.moveError') }) + it('should keep failed node ids in clipboard and still refresh tree when paste partially succeeds', async () => { + mocks.workflowState.clipboard = { + operation: 'cut', + nodeIds: new Set(['node-ok', 'node-fail']), + } + mocks.moveMutateAsync + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('move failed')) + const treeRef = createTreeRef('target') + const treeData: AppAssetTreeResponse = { + children: [createTreeNode('node-ok', 'file'), createTreeNode('node-fail', 'file')], + } + + const { result } = renderHook(() => usePasteOperation({ treeRef, treeData })) + + await act(async () => { + await result.current.handlePaste() + }) + + expect(mocks.moveMutateAsync).toHaveBeenCalledTimes(2) + expect(mocks.workflowState.clearClipboard).not.toHaveBeenCalled() + expect(mocks.workflowState.cutNodes).toHaveBeenCalledWith(['node-fail']) + expect(mocks.emitTreeUpdate).toHaveBeenCalledTimes(1) + expect(mocks.toastError).toHaveBeenCalledWith('workflow.skillSidebar.menu.moveError') + }) + it('should prevent re-entrant paste while a paste is in progress', async () => { mocks.workflowState.clipboard = { operation: 'cut', diff --git a/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.ts b/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.ts index 0c2099ac0e..017e436726 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.ts +++ b/web/app/components/workflow/skill/hooks/file-tree/operations/use-paste-operation.ts @@ -84,20 +84,37 @@ export function usePasteOperation({ isPastingRef.current = true try { - await Promise.all( - nodeIdsArray.map(nodeId => - moveNodeAsync({ + const results = await Promise.allSettled( + nodeIdsArray.map(async (nodeId) => { + await moveNodeAsync({ appId, nodeId, payload: { parent_id: targetParentId }, - }), - ), + }) + return nodeId + }), ) - storeApi.getState().clearClipboard() - emitTreeUpdate() + const succeededNodeIds = results.flatMap(result => + result.status === 'fulfilled' ? [result.value] : [], + ) + const failedNodeIds = results.flatMap((result, index) => + result.status === 'rejected' ? [nodeIdsArray[index]] : [], + ) - toast.success(t('skillSidebar.menu.moved')) + if (succeededNodeIds.length > 0) + emitTreeUpdate() + + if (failedNodeIds.length === 0) { + storeApi.getState().clearClipboard() + toast.success(t('skillSidebar.menu.moved')) + return + } + + if (succeededNodeIds.length > 0) + storeApi.getState().cutNodes(failedNodeIds) + + toast.error(t('skillSidebar.menu.moveError')) } catch { toast.error(t('skillSidebar.menu.moveError')) diff --git a/web/app/components/workflow/skill/skill-body/sidebar-search-add.spec.tsx b/web/app/components/workflow/skill/skill-body/sidebar-search-add.spec.tsx index 759a8f4a36..30993e7229 100644 --- a/web/app/components/workflow/skill/skill-body/sidebar-search-add.spec.tsx +++ b/web/app/components/workflow/skill/skill-body/sidebar-search-add.spec.tsx @@ -133,6 +133,7 @@ describe('SidebarSearchAdd', () => { it('should call create handlers when clicking new file and new folder actions', () => { // Arrange + mocks.storeState.fileTreeSearchTerm = 'agent' render() fireEvent.click(screen.getByRole('button', { name: /common\.operation\.add/i })) @@ -141,6 +142,8 @@ describe('SidebarSearchAdd', () => { fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFolder/i })) // Assert + expect(mocks.setFileTreeSearchTerm).toHaveBeenNthCalledWith(1, '') + expect(mocks.setFileTreeSearchTerm).toHaveBeenNthCalledWith(2, '') expect(mocks.fileOperations.handleNewFile).toHaveBeenCalledTimes(1) expect(mocks.fileOperations.handleNewFolder).toHaveBeenCalledTimes(1) }) diff --git a/web/app/components/workflow/skill/skill-body/sidebar-search-add.tsx b/web/app/components/workflow/skill/skill-body/sidebar-search-add.tsx index 624e3c97af..8fc94093db 100644 --- a/web/app/components/workflow/skill/skill-body/sidebar-search-add.tsx +++ b/web/app/components/workflow/skill/skill-body/sidebar-search-add.tsx @@ -57,6 +57,16 @@ const SidebarSearchAdd = () => { onClose: handleMenuClose, }) + const handleCreateFile = useCallback(() => { + storeApi.getState().setFileTreeSearchTerm('') + handleNewFile() + }, [handleNewFile, storeApi]) + + const handleCreateFolder = useCallback(() => { + storeApi.getState().setFileTreeSearchTerm('') + handleNewFolder() + }, [handleNewFolder, storeApi]) + return (
{ menuType="dropdown" icon={FileAdd} label={t('skillSidebar.menu.newFile')} - onClick={handleNewFile} + onClick={handleCreateFile} disabled={isLoading} />