mirror of
https://github.com/langgenius/dify.git
synced 2026-03-12 10:38:54 +08:00
feat(web): add import skills menu item with tooltip to skill file tree
Add "Import skills(.zip)" option to root-level context menu and sidebar add menu with a question mark tooltip showing usage hint. Update menu item labels and icons for consistency with design.
This commit is contained in:
@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -55,9 +57,10 @@ export type MenuItemProps = {
|
||||
kbd?: readonly string[]
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>
|
||||
disabled?: boolean
|
||||
tooltip?: string
|
||||
} & VariantProps<typeof menuItemVariants>
|
||||
|
||||
const MenuItem = ({ icon: Icon, label, kbd, onClick, disabled, variant }: MenuItemProps) => {
|
||||
const MenuItem = ({ icon: Icon, label, kbd, onClick, disabled, variant, tooltip }: MenuItemProps) => {
|
||||
const handleClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation()
|
||||
onClick(event)
|
||||
@ -73,6 +76,20 @@ const MenuItem = ({ icon: Icon, label, kbd, onClick, disabled, variant }: MenuIt
|
||||
<Icon className={cn(iconVariants({ variant }))} aria-hidden="true" />
|
||||
<span className={cn(labelVariants({ variant }), 'flex-1 text-left')}>{label}</span>
|
||||
{kbd && kbd.length > 0 && <ShortcutsName keys={kbd} textColor="secondary" />}
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
popupContent={tooltip}
|
||||
position="right"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<RiQuestionLine className="size-4 text-text-quaternary hover:text-text-tertiary" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -13,10 +13,12 @@ import {
|
||||
RiScissorsLine,
|
||||
RiUploadLine,
|
||||
} from '@remixicon/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
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'
|
||||
@ -24,6 +26,10 @@ import { NODE_MENU_TYPE } from '../constants'
|
||||
import { useFileOperations } from '../hooks/use-file-operations'
|
||||
import MenuItem from './menu-item'
|
||||
|
||||
const ImportSkillModal = dynamic(() => import('../start-tab/import-skill-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export 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]',
|
||||
@ -55,6 +61,7 @@ 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,
|
||||
@ -134,7 +141,7 @@ const NodeMenu = ({
|
||||
<div className="my-1 h-px bg-divider-subtle" />
|
||||
|
||||
<MenuItem
|
||||
icon={RiUploadLine}
|
||||
icon={UploadCloud02}
|
||||
label={t('skillSidebar.menu.uploadFile')}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
@ -146,6 +153,19 @@ const NodeMenu = ({
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{isRoot && (
|
||||
<>
|
||||
<div className="my-1 h-px bg-divider-subtle" />
|
||||
<MenuItem
|
||||
icon={RiUploadLine}
|
||||
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" />}
|
||||
</>
|
||||
)}
|
||||
@ -212,6 +232,10 @@ const NodeMenu = ({
|
||||
onCancel={handleDeleteCancel}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
<ImportSkillModal
|
||||
isOpen={isImportModalOpen}
|
||||
onClose={() => setIsImportModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,10 +7,12 @@ import {
|
||||
RiFolderUploadLine,
|
||||
RiUploadLine,
|
||||
} from '@remixicon/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { UploadCloud02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
@ -18,42 +20,22 @@ import {
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { ROOT_ID } from './constants'
|
||||
import MenuItem from './file-tree/menu-item'
|
||||
import { useFileOperations } from './hooks/use-file-operations'
|
||||
import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree'
|
||||
import { getTargetFolderIdFromSelection } from './utils/tree-utils'
|
||||
|
||||
type MenuItemProps = {
|
||||
icon: React.ElementType
|
||||
label: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const MenuItem = ({ icon: Icon, label, onClick, disabled }: MenuItemProps) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-lg px-3 py-2',
|
||||
'hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4 text-text-tertiary" aria-hidden="true" />
|
||||
<span className="system-sm-regular text-text-secondary">
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
const ImportSkillModal = dynamic(() => import('./start-tab/import-skill-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
const SidebarSearchAdd = () => {
|
||||
const { t } = useTranslation('workflow')
|
||||
const searchValue = useStore(s => s.fileTreeSearchTerm)
|
||||
const storeApi = useWorkflowStore()
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false)
|
||||
|
||||
const { data: treeData } = useSkillAssetTreeData()
|
||||
const selectedTreeNodeId = useStore(s => s.selectedTreeNodeId)
|
||||
@ -137,7 +119,7 @@ const SidebarSearchAdd = () => {
|
||||
<div className="my-1 h-px bg-divider-subtle" />
|
||||
|
||||
<MenuItem
|
||||
icon={RiUploadLine}
|
||||
icon={UploadCloud02}
|
||||
label={t('skillSidebar.menu.uploadFile')}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
@ -148,9 +130,23 @@ const SidebarSearchAdd = () => {
|
||||
onClick={() => folderInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<div className="my-1 h-px bg-divider-subtle" />
|
||||
|
||||
<MenuItem
|
||||
icon={RiUploadLine}
|
||||
label={t('skillSidebar.menu.importSkills')}
|
||||
onClick={() => setIsImportModalOpen(true)}
|
||||
disabled={isLoading}
|
||||
tooltip={t('skill.startTab.importSkillDesc')}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
<ImportSkillModal
|
||||
isOpen={isImportModalOpen}
|
||||
onClose={() => setIsImportModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1147,19 +1147,20 @@
|
||||
"skillSidebar.menu.folderCreated": "Folder created successfully",
|
||||
"skillSidebar.menu.folderDropNotSupported": "Folder upload via drag-drop is not supported yet. Please use the upload folder option.",
|
||||
"skillSidebar.menu.folderUploaded": "Folder uploaded successfully",
|
||||
"skillSidebar.menu.importSkills": "Import skills(.zip)",
|
||||
"skillSidebar.menu.moreActions": "More actions",
|
||||
"skillSidebar.menu.moveError": "Failed to move",
|
||||
"skillSidebar.menu.moved": "Moved successfully",
|
||||
"skillSidebar.menu.newFile": "New File",
|
||||
"skillSidebar.menu.newFile": "New file",
|
||||
"skillSidebar.menu.newFilePrompt": "Enter file name (with extension, e.g., script.py):",
|
||||
"skillSidebar.menu.newFolder": "New Folder",
|
||||
"skillSidebar.menu.newFolder": "New folder...",
|
||||
"skillSidebar.menu.newFolderPrompt": "Enter folder name:",
|
||||
"skillSidebar.menu.paste": "Paste",
|
||||
"skillSidebar.menu.rename": "Rename",
|
||||
"skillSidebar.menu.renameError": "Failed to rename",
|
||||
"skillSidebar.menu.renamed": "Renamed successfully",
|
||||
"skillSidebar.menu.uploadError": "Failed to upload",
|
||||
"skillSidebar.menu.uploadFile": "Upload File",
|
||||
"skillSidebar.menu.uploadFile": "Upload files...",
|
||||
"skillSidebar.menu.uploadFolder": "Upload Folder",
|
||||
"skillSidebar.newFolder": "New folder",
|
||||
"skillSidebar.renameFileInput": "Rename file",
|
||||
|
||||
@ -1139,18 +1139,19 @@
|
||||
"skillSidebar.menu.folderCreated": "文件夹创建成功",
|
||||
"skillSidebar.menu.folderDropNotSupported": "暂不支持拖拽上传文件夹,请使用上传文件夹选项。",
|
||||
"skillSidebar.menu.folderUploaded": "文件夹上传成功",
|
||||
"skillSidebar.menu.importSkills": "导入技能(.zip)",
|
||||
"skillSidebar.menu.moveError": "移动失败",
|
||||
"skillSidebar.menu.moved": "移动成功",
|
||||
"skillSidebar.menu.newFile": "新建文件",
|
||||
"skillSidebar.menu.newFilePrompt": "请输入文件名(包含扩展名,如 script.py):",
|
||||
"skillSidebar.menu.newFolder": "新建文件夹",
|
||||
"skillSidebar.menu.newFolder": "新建文件夹...",
|
||||
"skillSidebar.menu.newFolderPrompt": "请输入文件夹名称:",
|
||||
"skillSidebar.menu.paste": "粘贴",
|
||||
"skillSidebar.menu.rename": "重命名",
|
||||
"skillSidebar.menu.renameError": "重命名失败",
|
||||
"skillSidebar.menu.renamed": "重命名成功",
|
||||
"skillSidebar.menu.uploadError": "上传失败",
|
||||
"skillSidebar.menu.uploadFile": "上传文件",
|
||||
"skillSidebar.menu.uploadFile": "上传文件...",
|
||||
"skillSidebar.menu.uploadFolder": "上传文件夹",
|
||||
"skillSidebar.newFolder": "新建文件夹",
|
||||
"skillSidebar.renameFileInput": "重命名文件",
|
||||
|
||||
Reference in New Issue
Block a user