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 80de905352..f6bd486ea2 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 @@ -26,8 +26,6 @@ type MockFileOperations = { handleDownload: () => void handleNewFile: () => void handleNewFolder: () => void - handleFileChange: () => void - handleFolderChange: () => void handleRename: () => void handleDeleteClick: () => void handleDeleteConfirm: () => void @@ -53,8 +51,6 @@ function createFileOperationsMock(): MockFileOperations { handleDownload: vi.fn(), handleNewFile: vi.fn(), handleNewFolder: vi.fn(), - handleFileChange: vi.fn(), - handleFolderChange: vi.fn(), handleRename: vi.fn(), handleDeleteClick: vi.fn(), handleDeleteConfirm: vi.fn(), @@ -106,8 +102,6 @@ const renderNodeMenu = ({ onDownload={mocks.fileOperations.handleDownload} onNewFile={mocks.fileOperations.handleNewFile} onNewFolder={mocks.fileOperations.handleNewFolder} - onFileChange={mocks.fileOperations.handleFileChange} - onFolderChange={mocks.fileOperations.handleFolderChange} onRename={mocks.fileOperations.handleRename} onDeleteClick={mocks.fileOperations.handleDeleteClick} onImportSkills={onImportSkills} @@ -131,8 +125,6 @@ const renderNodeMenu = ({ onDownload={mocks.fileOperations.handleDownload} onNewFile={mocks.fileOperations.handleNewFile} onNewFolder={mocks.fileOperations.handleNewFolder} - onFileChange={mocks.fileOperations.handleFileChange} - onFolderChange={mocks.fileOperations.handleFolderChange} onRename={mocks.fileOperations.handleRename} onDeleteClick={mocks.fileOperations.handleDeleteClick} onImportSkills={onImportSkills} @@ -205,13 +197,20 @@ describe('NodeMenu', () => { }) it('should trigger hidden file and folder input clicks from upload actions', () => { - const clickSpy = vi.spyOn(HTMLInputElement.prototype, 'click') + const fileInput = document.createElement('input') + const folderInput = document.createElement('input') + const fileClickSpy = vi.spyOn(fileInput, 'click') + const folderClickSpy = vi.spyOn(folderInput, 'click') + mocks.fileOperations.fileInputRef.current = fileInput + mocks.fileOperations.folderInputRef.current = folderInput + renderNodeMenu({ type: NODE_MENU_TYPE.FOLDER }) fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFile/i })) fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFolder/i })) - expect(clickSpy).toHaveBeenCalledTimes(2) + expect(fileClickSpy).toHaveBeenCalledTimes(1) + expect(folderClickSpy).toHaveBeenCalledTimes(1) }) it('should cut explicit action node ids and close menu when cut is clicked', () => { 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 58295ef10d..b8b5b4a77b 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 @@ -32,8 +32,6 @@ type NodeMenuProps = { onDownload: () => void onNewFile: () => void onNewFolder: () => void - onFileChange: React.ChangeEventHandler - onFolderChange: React.ChangeEventHandler onRename: () => void onDeleteClick: () => void onImportSkills?: () => void @@ -51,8 +49,6 @@ const NodeMenu = ({ onDownload, onNewFile, onNewFolder, - onFileChange, - onFolderChange, onRename, onDeleteClick, onImportSkills, @@ -97,24 +93,6 @@ const NodeMenu = ({ <> {isFolder && ( <> - - - import('../../start-tab/import-skill-modal'), { ssr: false, @@ -106,6 +107,12 @@ const TreeContextMenu = ({ return ( <> + & { treeChildren: TreeNodeData[] @@ -101,6 +102,12 @@ const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => { return ( <> +
{ onDownload={fileOperations.handleDownload} onNewFile={fileOperations.handleNewFile} onNewFolder={fileOperations.handleNewFolder} - onFileChange={fileOperations.handleFileChange} - onFolderChange={fileOperations.handleFolderChange} onRename={fileOperations.handleRename} onDeleteClick={fileOperations.handleDeleteClick} /> diff --git a/web/app/components/workflow/skill/file-tree/tree/upload-inputs.tsx b/web/app/components/workflow/skill/file-tree/tree/upload-inputs.tsx new file mode 100644 index 0000000000..e09e434500 --- /dev/null +++ b/web/app/components/workflow/skill/file-tree/tree/upload-inputs.tsx @@ -0,0 +1,44 @@ +'use client' + +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +type UploadInputsProps = { + fileInputRef: React.RefObject + folderInputRef: React.RefObject + onFileChange: React.ChangeEventHandler + onFolderChange: React.ChangeEventHandler +} + +const UploadInputs = ({ + fileInputRef, + folderInputRef, + onFileChange, + onFolderChange, +}: UploadInputsProps) => { + const { t } = useTranslation('workflow') + + return ( + <> + + + + ) +} + +export default React.memo(UploadInputs) 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 30993e7229..756592db01 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 @@ -162,6 +162,27 @@ describe('SidebarSearchAdd', () => { expect(clickSpy).toHaveBeenCalledTimes(2) }) + it('should keep upload inputs mounted after menu closes so change handlers still run', () => { + // Arrange + render() + fireEvent.click(screen.getByRole('button', { name: /common\.operation\.add/i })) + + // Act + fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFile/i })) + + // Assert + const uploadFileInput = document.querySelector('input[type="file"][multiple]') as HTMLInputElement | null + expect(uploadFileInput).not.toBeNull() + + fireEvent.change(uploadFileInput!, { + target: { + files: [new File(['content'], 'readme.md', { type: 'text/markdown' })], + }, + }) + + expect(mocks.fileOperations.handleFileChange).toHaveBeenCalledTimes(1) + }) + it('should open and close import modal when import skills action is used', () => { // Arrange render() 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 8fc94093db..f1d29588c3 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 @@ -18,6 +18,7 @@ import dynamic from '@/next/dynamic' import { cn } from '@/utils/classnames' import { ROOT_ID } from '../constants' import MenuItem from '../file-tree/tree/menu-item' +import UploadInputs from '../file-tree/tree/upload-inputs' import { useSkillAssetTreeData } from '../hooks/file-tree/data/use-skill-asset-tree' import { useFileOperations } from '../hooks/file-tree/operations/use-file-operations' import { getTargetFolderIdFromSelection } from '../utils/tree-utils' @@ -69,6 +70,12 @@ const SidebarSearchAdd = () => { return (
+ storeApi.getState().setFileTreeSearchTerm(v)} @@ -91,22 +98,6 @@ const SidebarSearchAdd = () => { sideOffset={4} popupClassName="min-w-[180px]" > - - -