mirror of
https://github.com/langgenius/dify.git
synced 2026-04-21 03:07:39 +08:00
feat(skill-editor): add blank area context menu and align search/add styles
Add right-click context menu for file tree blank area with New File, New Folder, and Upload Files options. Also align search input and add button styles to match Figma design specs (24px height, 6px radius).
This commit is contained in:
@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
RiFileAddLine,
|
||||
RiFolderAddLine,
|
||||
RiUploadLine,
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useFileOperations } from '../hooks/use-file-operations'
|
||||
import MenuItem from './menu-item'
|
||||
|
||||
type BlankAreaMenuProps = {
|
||||
onClose: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const BlankAreaMenu: FC<BlankAreaMenuProps> = ({
|
||||
onClose,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
|
||||
const {
|
||||
fileInputRef,
|
||||
isLoading,
|
||||
handleNewFile,
|
||||
handleNewFolder,
|
||||
handleFileChange,
|
||||
} = useFileOperations({ nodeId: 'root', onClose })
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'min-w-[180px] rounded-xl border-[0.5px] border-components-panel-border',
|
||||
'bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
icon={RiFileAddLine}
|
||||
label={t('skillSidebar.menu.newFile')}
|
||||
onClick={handleNewFile}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={RiFolderAddLine}
|
||||
label={t('skillSidebar.menu.newFolder')}
|
||||
onClick={handleNewFolder}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<div className="my-1 h-px bg-divider-subtle" />
|
||||
|
||||
<MenuItem
|
||||
icon={RiUploadLine}
|
||||
label={t('skillSidebar.menu.uploadFile')}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(BlankAreaMenu)
|
||||
@ -91,6 +91,15 @@ const FileTree: React.FC<FileTreeProps> = ({ className, searchTerm = '' }) => {
|
||||
})
|
||||
}, [appId, renameNode, t])
|
||||
|
||||
const handleBlankAreaContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
storeApi.getState().setContextMenu({
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
type: 'blank',
|
||||
})
|
||||
}, [storeApi])
|
||||
|
||||
const searchMatch = useCallback(
|
||||
(node: NodeApi<TreeNodeData>, term: string) => {
|
||||
return node.data.name.toLowerCase().includes(term.toLowerCase())
|
||||
@ -146,6 +155,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className, searchTerm = '' }) => {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden px-1 pt-1"
|
||||
onContextMenu={handleBlankAreaContextMenu}
|
||||
>
|
||||
<Tree<TreeNodeData>
|
||||
ref={treeRef}
|
||||
|
||||
@ -9,6 +9,7 @@ import { useCallback, useMemo, useRef } from 'react'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree'
|
||||
import { findNodeById } from '../utils/tree-utils'
|
||||
import BlankAreaMenu from './blank-area-menu'
|
||||
import NodeMenu from './node-menu'
|
||||
|
||||
type TreeContextMenuProps = {
|
||||
@ -29,13 +30,17 @@ const TreeContextMenu: FC<TreeContextMenuProps> = ({ treeRef }) => {
|
||||
handleClose()
|
||||
}, ref)
|
||||
|
||||
const nodeId = contextMenu?.nodeId
|
||||
const treeChildren = treeData?.children
|
||||
|
||||
const targetNode = useMemo(() => {
|
||||
if (!contextMenu?.nodeId || !treeData?.children)
|
||||
if (!nodeId || !treeChildren)
|
||||
return null
|
||||
return findNodeById(treeData.children, contextMenu.nodeId)
|
||||
}, [contextMenu?.nodeId, treeData?.children])
|
||||
return findNodeById(treeChildren, nodeId)
|
||||
}, [nodeId, treeChildren])
|
||||
|
||||
const isFolder = targetNode?.node_type === 'folder'
|
||||
const isBlankArea = contextMenu?.type === 'blank'
|
||||
|
||||
if (!contextMenu)
|
||||
return null
|
||||
@ -49,12 +54,18 @@ const TreeContextMenu: FC<TreeContextMenuProps> = ({ treeRef }) => {
|
||||
left: contextMenu.left,
|
||||
}}
|
||||
>
|
||||
<NodeMenu
|
||||
type={isFolder ? 'folder' : 'file'}
|
||||
nodeId={contextMenu.nodeId}
|
||||
onClose={handleClose}
|
||||
treeRef={treeRef}
|
||||
/>
|
||||
{isBlankArea
|
||||
? (
|
||||
<BlankAreaMenu onClose={handleClose} />
|
||||
)
|
||||
: (
|
||||
<NodeMenu
|
||||
type={isFolder ? 'folder' : 'file'}
|
||||
nodeId={contextMenu.nodeId}
|
||||
onClose={handleClose}
|
||||
treeRef={treeRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -76,6 +76,7 @@ export function useTreeNodeHandlers({
|
||||
storeApi.getState().setContextMenu({
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
type: 'node',
|
||||
nodeId: node.data.id,
|
||||
})
|
||||
}, [node.data.id, storeApi])
|
||||
|
||||
@ -66,12 +66,13 @@ const SidebarSearchAdd: FC<SidebarSearchAddProps> = ({ onSearchChange }) => {
|
||||
|
||||
const { data: treeData } = useSkillAssetTreeData()
|
||||
const activeTabId = useStore(s => s.activeTabId)
|
||||
const treeChildren = treeData?.children
|
||||
|
||||
const targetFolderId = useMemo(() => {
|
||||
if (!treeData?.children)
|
||||
if (!treeChildren)
|
||||
return 'root'
|
||||
return getTargetFolderIdFromSelection(activeTabId, treeData.children)
|
||||
}, [activeTabId, treeData?.children])
|
||||
return getTargetFolderIdFromSelection(activeTabId, treeChildren)
|
||||
}, [activeTabId, treeChildren])
|
||||
const menuOffset = useMemo(() => ({ mainAxis: 4 }), [])
|
||||
|
||||
const {
|
||||
@ -88,11 +89,11 @@ const SidebarSearchAdd: FC<SidebarSearchAddProps> = ({ onSearchChange }) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 bg-components-panel-bg p-2">
|
||||
<div className="flex items-center gap-1 p-2">
|
||||
<SearchInput
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
className="h-8 flex-1"
|
||||
className="!h-6 flex-1 !rounded-md"
|
||||
placeholder={t('skillSidebar.searchPlaceholder')}
|
||||
/>
|
||||
<PortalToFollowElem
|
||||
@ -104,11 +105,11 @@ const SidebarSearchAdd: FC<SidebarSearchAddProps> = ({ onSearchChange }) => {
|
||||
<PortalToFollowElemTrigger onClick={() => setShowMenu(!showMenu)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
className={cn('!h-8 !w-8 !px-0')}
|
||||
size="small"
|
||||
className="!size-6 shrink-0 !p-1"
|
||||
aria-label={t('operation.add', { ns: 'common' })}
|
||||
>
|
||||
<RiAddLine className="h-4 w-4" aria-hidden="true" />
|
||||
<RiAddLine className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[30]">
|
||||
|
||||
@ -43,13 +43,18 @@ export type MetadataSliceShape = {
|
||||
getFileMetadata: (fileId: string) => Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
export type ContextMenuType = 'node' | 'blank'
|
||||
|
||||
export type ContextMenuState = {
|
||||
top: number
|
||||
left: number
|
||||
type: ContextMenuType
|
||||
nodeId?: string
|
||||
}
|
||||
|
||||
export type FileOperationsMenuSliceShape = {
|
||||
contextMenu: {
|
||||
top: number
|
||||
left: number
|
||||
nodeId: string
|
||||
} | null
|
||||
setContextMenu: (menu: FileOperationsMenuSliceShape['contextMenu']) => void
|
||||
contextMenu: ContextMenuState | null
|
||||
setContextMenu: (menu: ContextMenuState | null) => void
|
||||
}
|
||||
|
||||
export type SkillEditorSliceShape
|
||||
|
||||
Reference in New Issue
Block a user