mirror of
https://github.com/langgenius/dify.git
synced 2026-02-13 07:01:23 -05:00
Migrate all Remixicon component imports in workflow/skill to Tailwind CSS icon utility classes (i-ri-*), reducing JS bundle size. Update MenuItem to accept string icon classes alongside React components. Adjust test selectors that relied on SVG element queries.
235 lines
6.8 KiB
TypeScript
235 lines
6.8 KiB
TypeScript
'use client'
|
|
|
|
import type { NodeApi, TreeApi } from 'react-arborist'
|
|
import type { NodeMenuType } from '../../constants'
|
|
import type { TreeNodeData } from '../../type'
|
|
import dynamic from 'next/dynamic'
|
|
import * as React from 'react'
|
|
import { useCallback, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import Confirm from '@/app/components/base/confirm'
|
|
import { FileAdd, FolderAdd } from '@/app/components/base/icons/src/vender/line/files'
|
|
import { UploadCloud02 } from '@/app/components/base/icons/src/vender/line/general'
|
|
import { Download02 } from '@/app/components/base/icons/src/vender/solid/general'
|
|
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
|
import { cn } from '@/utils/classnames'
|
|
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 MENU_CONTAINER_STYLES = [
|
|
'min-w-[180px] rounded-xl border-[0.5px] border-components-panel-border',
|
|
'bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]',
|
|
] as const
|
|
|
|
const KBD_CUT = ['ctrl', 'x'] as const
|
|
const KBD_PASTE = ['ctrl', 'v'] as const
|
|
|
|
type NodeMenuProps = {
|
|
type: NodeMenuType
|
|
nodeId?: string
|
|
onClose: () => void
|
|
className?: string
|
|
treeRef?: React.RefObject<TreeApi<TreeNodeData> | null>
|
|
node?: NodeApi<TreeNodeData>
|
|
}
|
|
|
|
const NodeMenu = ({
|
|
type,
|
|
nodeId,
|
|
onClose,
|
|
className,
|
|
treeRef,
|
|
node,
|
|
}: NodeMenuProps) => {
|
|
const { t } = useTranslation('workflow')
|
|
const storeApi = useWorkflowStore()
|
|
const selectedNodeIds = useStore(s => s.selectedNodeIds)
|
|
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,
|
|
folderInputRef,
|
|
showDeleteConfirm,
|
|
isLoading,
|
|
isDeleting,
|
|
handleDownload,
|
|
handleNewFile,
|
|
handleNewFolder,
|
|
handleFileChange,
|
|
handleFolderChange,
|
|
handleRename,
|
|
handleDeleteClick,
|
|
handleDeleteConfirm,
|
|
handleDeleteCancel,
|
|
} = useFileOperations({ nodeId, onClose, treeRef, node })
|
|
|
|
const currentNodeId = node?.data.id ?? nodeId
|
|
|
|
const handleCut = useCallback(() => {
|
|
const ids = selectedNodeIds.size > 0 ? [...selectedNodeIds] : (currentNodeId ? [currentNodeId] : [])
|
|
if (ids.length > 0) {
|
|
storeApi.getState().cutNodes(ids)
|
|
onClose()
|
|
}
|
|
}, [currentNodeId, onClose, selectedNodeIds, storeApi])
|
|
|
|
const handlePaste = useCallback(() => {
|
|
window.dispatchEvent(new CustomEvent('skill:paste'))
|
|
onClose()
|
|
}, [onClose])
|
|
|
|
const showRenameDelete = isFolder ? !isRoot : true
|
|
const deleteConfirmTitle = isFolder
|
|
? t('skillSidebar.menu.deleteConfirmTitle')
|
|
: t('skillSidebar.menu.fileDeleteConfirmTitle')
|
|
const deleteConfirmContent = isFolder
|
|
? t('skillSidebar.menu.deleteConfirmContent')
|
|
: t('skillSidebar.menu.fileDeleteConfirmContent')
|
|
|
|
return (
|
|
<div className={cn(MENU_CONTAINER_STYLES, className)}>
|
|
{isFolder && (
|
|
<>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
multiple
|
|
className="hidden"
|
|
aria-label={t('skillSidebar.menu.uploadFile')}
|
|
onChange={handleFileChange}
|
|
/>
|
|
<input
|
|
ref={folderInputRef}
|
|
type="file"
|
|
// @ts-expect-error webkitdirectory is a non-standard attribute
|
|
webkitdirectory=""
|
|
className="hidden"
|
|
aria-label={t('skillSidebar.menu.uploadFolder')}
|
|
onChange={handleFolderChange}
|
|
/>
|
|
|
|
<MenuItem
|
|
icon={FileAdd}
|
|
label={t('skillSidebar.menu.newFile')}
|
|
onClick={handleNewFile}
|
|
disabled={isLoading}
|
|
/>
|
|
<MenuItem
|
|
icon={FolderAdd}
|
|
label={t('skillSidebar.menu.newFolder')}
|
|
onClick={handleNewFolder}
|
|
disabled={isLoading}
|
|
/>
|
|
|
|
<div className="my-1 h-px bg-divider-subtle" />
|
|
|
|
<MenuItem
|
|
icon={UploadCloud02}
|
|
label={t('skillSidebar.menu.uploadFile')}
|
|
onClick={() => fileInputRef.current?.click()}
|
|
disabled={isLoading}
|
|
/>
|
|
<MenuItem
|
|
icon="i-ri-folder-upload-line"
|
|
label={t('skillSidebar.menu.uploadFolder')}
|
|
onClick={() => folderInputRef.current?.click()}
|
|
disabled={isLoading}
|
|
/>
|
|
|
|
{isRoot && (
|
|
<>
|
|
<div className="my-1 h-px bg-divider-subtle" />
|
|
<MenuItem
|
|
icon="i-ri-upload-line"
|
|
label={t('skillSidebar.menu.importSkills')}
|
|
onClick={() => setIsImportModalOpen(true)}
|
|
disabled={isLoading}
|
|
tooltip={t('skill.startTab.importSkillDesc')}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{(showRenameDelete || hasClipboard) && <div className="my-1 h-px bg-divider-subtle" />}
|
|
</>
|
|
)}
|
|
|
|
{!isFolder && (
|
|
<>
|
|
<MenuItem
|
|
icon={Download02}
|
|
label={t('skillSidebar.menu.download')}
|
|
onClick={handleDownload}
|
|
disabled={isLoading}
|
|
/>
|
|
<div className="my-1 h-px bg-divider-subtle" />
|
|
</>
|
|
)}
|
|
|
|
{!isRoot && (
|
|
<>
|
|
<MenuItem
|
|
icon="i-ri-scissors-line"
|
|
label={t('skillSidebar.menu.cut')}
|
|
kbd={KBD_CUT}
|
|
onClick={handleCut}
|
|
disabled={isLoading}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{isFolder && hasClipboard && (
|
|
<MenuItem
|
|
icon="i-ri-clipboard-line"
|
|
label={t('skillSidebar.menu.paste')}
|
|
kbd={KBD_PASTE}
|
|
onClick={handlePaste}
|
|
disabled={isLoading}
|
|
/>
|
|
)}
|
|
|
|
{showRenameDelete && (
|
|
<>
|
|
<div className="my-1 h-px bg-divider-subtle" />
|
|
<MenuItem
|
|
icon="i-ri-edit-2-line"
|
|
label={t('skillSidebar.menu.rename')}
|
|
onClick={handleRename}
|
|
disabled={isLoading}
|
|
/>
|
|
<MenuItem
|
|
icon="i-ri-delete-bin-line"
|
|
label={t('skillSidebar.menu.delete')}
|
|
onClick={handleDeleteClick}
|
|
disabled={isLoading}
|
|
variant="destructive"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<Confirm
|
|
isShow={showDeleteConfirm}
|
|
type="danger"
|
|
title={deleteConfirmTitle}
|
|
content={deleteConfirmContent}
|
|
onConfirm={handleDeleteConfirm}
|
|
onCancel={handleDeleteCancel}
|
|
isLoading={isDeleting}
|
|
/>
|
|
<ImportSkillModal
|
|
isOpen={isImportModalOpen}
|
|
onClose={() => setIsImportModalOpen(false)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(NodeMenu)
|