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 db63c23d44..65e70c842a 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 @@ -1,4 +1,4 @@ -import type { ReactElement, RefObject } from 'react' +import type { RefObject } from 'react' import type { NodeApi, TreeApi } from 'react-arborist' import type { TreeNodeData } from '../../type' import { fireEvent, render, screen } from '@testing-library/react' @@ -42,6 +42,7 @@ type RenderNodeMenuProps = { menuType?: 'dropdown' | 'context' nodeId?: string onClose?: () => void + onImportSkills?: () => void treeRef?: RefObject | null> node?: NodeApi } @@ -74,23 +75,6 @@ const mocks = vi.hoisted(() => ({ fileOperations: createFileOperationsMock(), })) -vi.mock('next/dynamic', () => ({ - default: () => { - const MockImportSkillModal = ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }): ReactElement | null => { - if (!isOpen) - return null - - return ( -
- -
- ) - } - - return MockImportSkillModal - }, -})) - vi.mock('@/app/components/workflow/store', () => ({ useStore: (selector: (state: MockWorkflowState) => unknown) => selector(mocks.storeState), useWorkflowStore: () => ({ @@ -109,6 +93,7 @@ const renderNodeMenu = ({ menuType = 'dropdown', nodeId = 'node-1', onClose = vi.fn(), + onImportSkills, treeRef, node, }: RenderNodeMenuProps = {}) => { @@ -122,6 +107,7 @@ const renderNodeMenu = ({ menuType={menuType} nodeId={nodeId} onClose={onClose} + onImportSkills={onImportSkills} treeRef={treeRef} node={node} /> @@ -137,6 +123,7 @@ const renderNodeMenu = ({ menuType={menuType} nodeId={nodeId} onClose={onClose} + onImportSkills={onImportSkills} treeRef={treeRef} node={node} /> @@ -263,14 +250,12 @@ describe('NodeMenu', () => { }) describe('Dialogs', () => { - it('should open and close import modal from root menu', () => { - renderNodeMenu({ type: NODE_MENU_TYPE.ROOT }) + it('should call import handler from root menu', () => { + const onImportSkills = vi.fn() + renderNodeMenu({ type: NODE_MENU_TYPE.ROOT, onImportSkills }) fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.importSkills/i })) - expect(screen.getByTestId('import-skill-modal')).toBeInTheDocument() - - fireEvent.click(screen.getByRole('button', { name: /close-import-modal/i })) - expect(screen.queryByTestId('import-skill-modal')).not.toBeInTheDocument() + expect(onImportSkills).toHaveBeenCalledTimes(1) }) it('should render delete confirmation content for files and forward confirm callbacks', () => { 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 2ab804f3ff..875cf90027 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 @@ -4,7 +4,7 @@ import type { NodeApi, TreeApi } from 'react-arborist' import type { NodeMenuType } from '../../constants' import type { TreeNodeData } from '../../type' import * as React from 'react' -import { useCallback, useState } from 'react' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { FileAdd, FolderAdd } from '@/app/components/base/icons/src/vender/line/files' import { UploadCloud02 } from '@/app/components/base/icons/src/vender/line/general' @@ -25,15 +25,10 @@ import { DropdownMenuSeparator, } from '@/app/components/base/ui/dropdown-menu' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import dynamic from '@/next/dynamic' import { NODE_MENU_TYPE } from '../../constants' import { useFileOperations } from '../../hooks/file-tree/operations/use-file-operations' import MenuItem from './menu-item' -const ImportSkillModal = dynamic(() => import('../../start-tab/import-skill-modal'), { - ssr: false, -}) - const KBD_CUT = ['ctrl', 'x'] as const const KBD_PASTE = ['ctrl', 'v'] as const @@ -44,6 +39,7 @@ type NodeMenuProps = { onClose: () => void treeRef?: React.RefObject | null> node?: NodeApi + onImportSkills?: () => void } const NodeMenu = ({ @@ -53,6 +49,7 @@ const NodeMenu = ({ onClose, treeRef, node, + onImportSkills, }: NodeMenuProps) => { const { t } = useTranslation('workflow') const storeApi = useWorkflowStore() @@ -60,7 +57,6 @@ const NodeMenu = ({ const hasClipboard = useStore(s => s.hasClipboard()) const isRoot = type === NODE_MENU_TYPE.ROOT const isFolder = type === NODE_MENU_TYPE.FOLDER || isRoot - const [isImportModalOpen, setIsImportModalOpen] = useState(false) const { fileInputRef, @@ -164,7 +160,7 @@ const NodeMenu = ({ menuType={menuType} icon="i-ri-upload-line" label={t('skillSidebar.menu.importSkills')} - onClick={() => setIsImportModalOpen(true)} + onClick={() => onImportSkills?.()} disabled={isLoading} tooltip={t('skill.startTab.importSkillDesc')} /> @@ -264,10 +260,6 @@ const NodeMenu = ({ - setIsImportModalOpen(false)} - /> ) } 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 6c3f75398d..7b58221de6 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 @@ -15,13 +15,38 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) +vi.mock('next/dynamic', () => ({ + default: () => { + const MockImportSkillModal = ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) => { + if (!isOpen) + return null + + return ( +
+ +
+ ) + } + + return MockImportSkillModal + }, +})) + vi.mock('./node-menu', () => ({ - default: ({ type, menuType, nodeId }: { type: string, menuType: string, nodeId?: string }) => ( + default: ({ type, menuType, nodeId, onImportSkills }: { type: string, menuType: string, nodeId?: string, onImportSkills?: () => void }) => (
+ > + {onImportSkills && ( + + )} +
), })) @@ -57,5 +82,21 @@ describe('TreeContextMenu', () => { expect(screen.getByTestId('node-menu-context')).toHaveAttribute('data-type', 'root') expect(screen.getByTestId('node-menu-context')).toHaveAttribute('data-node-id', ROOT_ID) }) + + it('should keep import modal mounted after root menu requests it', () => { + render( + +
blank area
+
, + ) + + fireEvent.contextMenu(screen.getByText('blank area')) + fireEvent.click(screen.getByRole('button', { name: /open-import-skill-modal/i })) + + expect(screen.getByTestId('import-skill-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByText(/close-import-modal/i)) + expect(screen.queryByTestId('import-skill-modal')).not.toBeInTheDocument() + }) }) }) diff --git a/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.tsx b/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.tsx index 54552ee91b..0673dc714f 100644 --- a/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.tsx +++ b/web/app/components/workflow/skill/file-tree/tree/tree-context-menu.tsx @@ -3,15 +3,21 @@ import type { TreeApi } from 'react-arborist' import type { TreeNodeData } from '../../type' import * as React from 'react' +import { useState } from 'react' import { ContextMenu, ContextMenuContent, ContextMenuTrigger, } from '@/app/components/base/ui/context-menu' import { useWorkflowStore } from '@/app/components/workflow/store' +import dynamic from '@/next/dynamic' import { NODE_MENU_TYPE, ROOT_ID } from '../../constants' import NodeMenu from './node-menu' +const ImportSkillModal = dynamic(() => import('../../start-tab/import-skill-modal'), { + ssr: false, +}) + type TreeContextMenuProps = Omit< React.ComponentPropsWithoutRef, 'children' | 'onContextMenu' @@ -28,6 +34,7 @@ const TreeContextMenu = ({ ...props }: TreeContextMenuProps) => { const storeApi = useWorkflowStore() + const [isImportModalOpen, setIsImportModalOpen] = useState(false) const handleContextMenu = React.useCallback((event: React.MouseEvent) => { const target = event.target as HTMLElement @@ -39,26 +46,36 @@ const TreeContextMenu = ({ }, [storeApi, treeRef]) const handleMenuClose = React.useCallback(() => {}, []) + const handleOpenImportSkills = React.useCallback(() => { + setIsImportModalOpen(true) + }, []) return ( - - - {children} - - - - - + <> + + + {children} + + + + + + setIsImportModalOpen(false)} + /> + ) }