fix: import skill modal

This commit is contained in:
yyh 2026-03-24 20:18:38 +08:00
parent a3cd497dc0
commit 7bc0ec8a36
No known key found for this signature in database
4 changed files with 91 additions and 56 deletions

View File

@ -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<TreeApi<TreeNodeData> | null>
node?: NodeApi<TreeNodeData>
}
@ -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 (
<div data-testid="import-skill-modal">
<button type="button" onClick={onClose}>close-import-modal</button>
</div>
)
}
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', () => {

View File

@ -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<TreeApi<TreeNodeData> | null>
node?: NodeApi<TreeNodeData>
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 = ({
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
<ImportSkillModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
/>
</>
)
}

View File

@ -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 (
<div data-testid="import-skill-modal">
<button type="button" onClick={onClose}>
close-import-modal
</button>
</div>
)
}
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 }) => (
<div
data-testid={`node-menu-${menuType}`}
data-type={type}
data-node-id={nodeId ?? ''}
/>
>
{onImportSkills && (
<button type="button" onClick={onImportSkills}>
open-import-skill-modal
</button>
)}
</div>
),
}))
@ -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(
<TreeContextMenu treeRef={{ current: { deselectAll: mocks.deselectAll } as never }}>
<div>blank area</div>
</TreeContextMenu>,
)
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()
})
})
})

View File

@ -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<typeof ContextMenuTrigger>,
'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<HTMLDivElement>) => {
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 (
<ContextMenu>
<ContextMenuTrigger
ref={triggerRef}
onContextMenu={handleContextMenu}
{...props}
>
{children}
</ContextMenuTrigger>
<ContextMenuContent popupClassName="min-w-[180px]">
<NodeMenu
menuType="context"
type={NODE_MENU_TYPE.ROOT}
nodeId={ROOT_ID}
onClose={handleMenuClose}
treeRef={treeRef}
/>
</ContextMenuContent>
</ContextMenu>
<>
<ContextMenu>
<ContextMenuTrigger
ref={triggerRef}
onContextMenu={handleContextMenu}
{...props}
>
{children}
</ContextMenuTrigger>
<ContextMenuContent popupClassName="min-w-[180px]">
<NodeMenu
menuType="context"
type={NODE_MENU_TYPE.ROOT}
nodeId={ROOT_ID}
onClose={handleMenuClose}
treeRef={treeRef}
onImportSkills={handleOpenImportSkills}
/>
</ContextMenuContent>
</ContextMenu>
<ImportSkillModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
/>
</>
)
}