mirror of
https://github.com/langgenius/dify.git
synced 2026-03-28 01:29:55 +08:00
fix(skill): keep upload inputs mounted outside overlays
This commit is contained in:
@ -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', () => {
|
||||
|
||||
@ -32,8 +32,6 @@ type NodeMenuProps = {
|
||||
onDownload: () => void
|
||||
onNewFile: () => void
|
||||
onNewFolder: () => void
|
||||
onFileChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
onFolderChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
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 && (
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
aria-label={t('skillSidebar.menu.uploadFile')}
|
||||
onChange={onFileChange}
|
||||
/>
|
||||
<input
|
||||
ref={folderInputRef}
|
||||
type="file"
|
||||
// @ts-expect-error webkitdirectory is a non-standard attribute
|
||||
webkitdirectory=""
|
||||
className="hidden"
|
||||
aria-label={t('skillSidebar.menu.uploadFolder')}
|
||||
onChange={onFolderChange}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
menuType={menuType}
|
||||
icon={FileAdd}
|
||||
|
||||
@ -15,6 +15,7 @@ import { NODE_MENU_TYPE, ROOT_ID } from '../../constants'
|
||||
import { useFileOperations } from '../../hooks/file-tree/operations/use-file-operations'
|
||||
import NodeDeleteConfirmDialog from './node-delete-confirm-dialog'
|
||||
import NodeMenu from './node-menu'
|
||||
import UploadInputs from './upload-inputs'
|
||||
|
||||
const ImportSkillModal = dynamic(() => import('../../start-tab/import-skill-modal'), {
|
||||
ssr: false,
|
||||
@ -106,6 +107,12 @@ const TreeContextMenu = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadInputs
|
||||
fileInputRef={fileOperations.fileInputRef}
|
||||
folderInputRef={fileOperations.folderInputRef}
|
||||
onFileChange={fileOperations.handleFileChange}
|
||||
onFolderChange={fileOperations.handleFolderChange}
|
||||
/>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger
|
||||
ref={triggerRef}
|
||||
@ -127,8 +134,6 @@ const TreeContextMenu = ({
|
||||
onDownload={fileOperations.handleDownload}
|
||||
onNewFile={fileOperations.handleNewFile}
|
||||
onNewFolder={fileOperations.handleNewFolder}
|
||||
onFileChange={fileOperations.handleFileChange}
|
||||
onFolderChange={fileOperations.handleFolderChange}
|
||||
onRename={fileOperations.handleRename}
|
||||
onDeleteClick={fileOperations.handleDeleteClick}
|
||||
onImportSkills={isRootTarget ? handleOpenImportSkills : undefined}
|
||||
|
||||
@ -21,6 +21,7 @@ import NodeMenu from './node-menu'
|
||||
import TreeEditInput from './tree-edit-input'
|
||||
import TreeGuideLines from './tree-guide-lines'
|
||||
import { TreeNodeIcon } from './tree-node-icon'
|
||||
import UploadInputs from './upload-inputs'
|
||||
|
||||
type TreeNodeProps = NodeRendererProps<TreeNodeData> & {
|
||||
treeChildren: TreeNodeData[]
|
||||
@ -101,6 +102,12 @@ const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadInputs
|
||||
fileInputRef={fileOperations.fileInputRef}
|
||||
folderInputRef={fileOperations.folderInputRef}
|
||||
onFileChange={fileOperations.handleFileChange}
|
||||
onFolderChange={fileOperations.handleFolderChange}
|
||||
/>
|
||||
<div
|
||||
ref={dragHandle}
|
||||
style={style}
|
||||
@ -195,8 +202,6 @@ const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => {
|
||||
onDownload={fileOperations.handleDownload}
|
||||
onNewFile={fileOperations.handleNewFile}
|
||||
onNewFolder={fileOperations.handleNewFolder}
|
||||
onFileChange={fileOperations.handleFileChange}
|
||||
onFolderChange={fileOperations.handleFolderChange}
|
||||
onRename={fileOperations.handleRename}
|
||||
onDeleteClick={fileOperations.handleDeleteClick}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type UploadInputsProps = {
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>
|
||||
folderInputRef: React.RefObject<HTMLInputElement | null>
|
||||
onFileChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
onFolderChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
}
|
||||
|
||||
const UploadInputs = ({
|
||||
fileInputRef,
|
||||
folderInputRef,
|
||||
onFileChange,
|
||||
onFolderChange,
|
||||
}: UploadInputsProps) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
aria-label={t('skillSidebar.menu.uploadFile')}
|
||||
onChange={onFileChange}
|
||||
/>
|
||||
<input
|
||||
ref={folderInputRef}
|
||||
type="file"
|
||||
// @ts-expect-error webkitdirectory is a non-standard attribute
|
||||
webkitdirectory=""
|
||||
className="hidden"
|
||||
aria-label={t('skillSidebar.menu.uploadFolder')}
|
||||
onChange={onFolderChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(UploadInputs)
|
||||
@ -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(<SidebarSearchAdd />)
|
||||
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(<SidebarSearchAdd />)
|
||||
|
||||
@ -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 (
|
||||
<div className="flex items-center gap-1 p-2">
|
||||
<UploadInputs
|
||||
fileInputRef={fileInputRef}
|
||||
folderInputRef={folderInputRef}
|
||||
onFileChange={handleFileChange}
|
||||
onFolderChange={handleFolderChange}
|
||||
/>
|
||||
<SearchInput
|
||||
value={searchValue}
|
||||
onChange={v => storeApi.getState().setFileTreeSearchTerm(v)}
|
||||
@ -91,22 +98,6 @@ const SidebarSearchAdd = () => {
|
||||
sideOffset={4}
|
||||
popupClassName="min-w-[180px]"
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<input
|
||||
ref={folderInputRef}
|
||||
type="file"
|
||||
// @ts-expect-error webkitdirectory is a non-standard attribute
|
||||
webkitdirectory=""
|
||||
className="hidden"
|
||||
onChange={handleFolderChange}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
menuType="dropdown"
|
||||
icon={FileAdd}
|
||||
|
||||
Reference in New Issue
Block a user