mirror of https://github.com/langgenius/dify.git
fix: import skill modal
This commit is contained in:
parent
a3cd497dc0
commit
7bc0ec8a36
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue