refactor(skill): merge BlankAreaMenu into NodeMenu

Consolidate menu components by extending NodeMenu to support a 'root'
type, eliminating the redundant BlankAreaMenu component. This reduces
code duplication and simplifies the context menu logic by storing
isFolder in the context menu state instead of re-querying tree data.
This commit is contained in:
yyh
2026-01-19 14:22:25 +08:00
parent 5947e04226
commit 9aed4f830f
5 changed files with 43 additions and 128 deletions

View File

@ -1,74 +0,0 @@
'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)

View File

@ -18,8 +18,13 @@ import { cn } from '@/utils/classnames'
import { useFileOperations } from '../hooks/use-file-operations'
import MenuItem from './menu-item'
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]',
] as const
type NodeMenuProps = {
type: 'file' | 'folder'
type: 'file' | 'folder' | 'root'
nodeId?: string
onClose: () => void
className?: string
@ -36,9 +41,8 @@ const NodeMenu: FC<NodeMenuProps> = ({
node,
}) => {
const { t } = useTranslation('workflow')
const isFolder = type === 'folder'
const derivedNodeId = node?.data.id ?? nodeId ?? ''
const isRoot = derivedNodeId === 'root'
const isRoot = type === 'root'
const isFolder = type === 'folder' || isRoot
const {
fileInputRef,
@ -65,12 +69,7 @@ const NodeMenu: FC<NodeMenuProps> = ({
: t('skillSidebar.menu.fileDeleteConfirmContent')
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,
)}
>
<div className={cn(MENU_CONTAINER_STYLES, className)}>
{isFolder && (
<>
<input
@ -80,14 +79,16 @@ const NodeMenu: FC<NodeMenuProps> = ({
className="hidden"
onChange={handleFileChange}
/>
<input
ref={folderInputRef}
type="file"
// @ts-expect-error webkitdirectory is a non-standard attribute
webkitdirectory=""
className="hidden"
onChange={handleFolderChange}
/>
{!isRoot && (
<input
ref={folderInputRef}
type="file"
// @ts-expect-error webkitdirectory is a non-standard attribute
webkitdirectory=""
className="hidden"
onChange={handleFolderChange}
/>
)}
<MenuItem
icon={RiFileAddLine}
@ -110,12 +111,14 @@ const NodeMenu: FC<NodeMenuProps> = ({
onClick={() => fileInputRef.current?.click()}
disabled={isLoading}
/>
<MenuItem
icon={RiFolderUploadLine}
label={t('skillSidebar.menu.uploadFolder')}
onClick={() => folderInputRef.current?.click()}
disabled={isLoading}
/>
{!isRoot && (
<MenuItem
icon={RiFolderUploadLine}
label={t('skillSidebar.menu.uploadFolder')}
onClick={() => folderInputRef.current?.click()}
disabled={isLoading}
/>
)}
{showRenameDelete && <div className="my-1 h-px bg-divider-subtle" />}
</>

View File

@ -5,22 +5,24 @@ import type { TreeApi } from 'react-arborist'
import type { TreeNodeData } from '../type'
import { useClickAway } from 'ahooks'
import * as React from 'react'
import { useCallback, useMemo, useRef } from 'react'
import { useCallback, 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 = {
treeRef: React.RefObject<TreeApi<TreeNodeData> | null>
}
function getMenuType(contextMenu: { type: string, isFolder?: boolean }): 'root' | 'folder' | 'file' {
if (contextMenu.type === 'blank')
return 'root'
return contextMenu.isFolder ? 'folder' : 'file'
}
const TreeContextMenu: FC<TreeContextMenuProps> = ({ treeRef }) => {
const ref = useRef<HTMLDivElement>(null)
const contextMenu = useStore(s => s.contextMenu)
const storeApi = useWorkflowStore()
const { data: treeData } = useSkillAssetTreeData()
const handleClose = useCallback(() => {
storeApi.getState().setContextMenu(null)
@ -30,18 +32,6 @@ const TreeContextMenu: FC<TreeContextMenuProps> = ({ treeRef }) => {
handleClose()
}, ref)
const nodeId = contextMenu?.nodeId
const treeChildren = treeData?.children
const targetNode = useMemo(() => {
if (!nodeId || !treeChildren)
return null
return findNodeById(treeChildren, nodeId)
}, [nodeId, treeChildren])
const isFolder = targetNode?.node_type === 'folder'
const isBlankArea = contextMenu?.type === 'blank'
if (!contextMenu)
return null
@ -54,18 +44,12 @@ const TreeContextMenu: FC<TreeContextMenuProps> = ({ treeRef }) => {
left: contextMenu.left,
}}
>
{isBlankArea
? (
<BlankAreaMenu onClose={handleClose} />
)
: (
<NodeMenu
type={isFolder ? 'folder' : 'file'}
nodeId={contextMenu.nodeId}
onClose={handleClose}
treeRef={treeRef}
/>
)}
<NodeMenu
type={getMenuType(contextMenu)}
nodeId={contextMenu.nodeId}
onClose={handleClose}
treeRef={treeRef}
/>
</div>
)
}

View File

@ -80,8 +80,9 @@ export function useTreeNodeHandlers({
left: e.clientX,
type: 'node',
nodeId: node.data.id,
isFolder,
})
}, [node.data.id, storeApi])
}, [isFolder, node.data.id, storeApi])
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {

View File

@ -63,6 +63,7 @@ export type ContextMenuState = {
left: number
type: ContextMenuType
nodeId?: string
isFolder?: boolean
}
export type FileOperationsMenuSliceShape = {